از یک پورت سریال بخوانید و بنویسید

رابط برنامه‌نویسی کاربردی سریال وب به وب‌سایت‌ها اجازه می‌دهد تا با دستگاه‌های سریال ارتباط برقرار کنند.

فرانسوا بوفور
François Beaufort

API سریال وب چیست؟

پورت سریال یک رابط ارتباطی دو طرفه است که امکان ارسال و دریافت داده را بایت به بایت فراهم می‌کند.

API سریال وب راهی را برای وب‌سایت‌ها فراهم می‌کند تا با استفاده از جاوا اسکریپت، از یک دستگاه سریال بخوانند و در آن بنویسند. دستگاه‌های سریال یا از طریق پورت سریال روی سیستم کاربر یا از طریق دستگاه‌های USB و بلوتوث قابل جابجایی که پورت سریال را شبیه‌سازی می‌کنند، متصل می‌شوند.

به عبارت دیگر، API سریال وب، با فراهم کردن امکان ارتباط وب‌سایت‌ها با دستگاه‌های سریال، مانند میکروکنترلرها و چاپگرهای سه‌بعدی، پلی بین وب و دنیای فیزیکی ایجاد می‌کند.

این API همچنین همراه بسیار خوبی برای WebUSB است زیرا سیستم‌عامل‌ها از برنامه‌ها می‌خواهند که با برخی از پورت‌های سریال با استفاده از API سریال سطح بالاتر خود به جای API USB سطح پایین ارتباط برقرار کنند.

موارد استفاده پیشنهادی

در بخش‌های آموزشی، سرگرمی و صنعتی، کاربران دستگاه‌های جانبی را به رایانه‌های خود متصل می‌کنند. این دستگاه‌ها اغلب توسط میکروکنترلرها با اتصال سریال که توسط نرم‌افزارهای سفارشی استفاده می‌شود، کنترل می‌شوند. برخی از نرم‌افزارهای سفارشی برای کنترل این دستگاه‌ها با فناوری وب ساخته شده‌اند:

در برخی موارد، وب‌سایت‌ها از طریق یک برنامه‌ی عامل که کاربران به صورت دستی نصب کرده‌اند، با دستگاه ارتباط برقرار می‌کنند. در برخی دیگر، برنامه در یک برنامه‌ی بسته‌بندی‌شده از طریق چارچوبی مانند Electron ارائه می‌شود. و در برخی دیگر، کاربر ملزم به انجام یک مرحله‌ی اضافی مانند کپی کردن یک برنامه‌ی کامپایل‌شده به دستگاه از طریق یک فلش درایو USB است.

در تمام این موارد، با فراهم کردن ارتباط مستقیم بین وب‌سایت و دستگاهی که آن را کنترل می‌کند، تجربه کاربری بهبود می‌یابد.

وضعیت فعلی

قدم وضعیت
۱. توضیح‌دهنده ایجاد کنید کامل
۲. پیش‌نویس اولیه مشخصات را ایجاد کنید کامل
۳. بازخوردها را جمع‌آوری کنید و روی طراحی تکرار کنید کامل
۴. آزمایش مبدا کامل
۵. راه‌اندازی کامل

استفاده از API سریال وب

تشخیص ویژگی

برای بررسی اینکه آیا Web Serial API پشتیبانی می‌شود یا خیر، از دستور زیر استفاده کنید:

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

باز کردن یک پورت سریال

API سریال وب به صورت ناهمزمان طراحی شده است. این امر مانع از مسدود شدن رابط کاربری وب‌سایت هنگام انتظار ورودی می‌شود، که مهم است زیرا داده‌های سریال را می‌توان در هر زمانی دریافت کرد و به روشی برای گوش دادن به آن نیاز است.

برای باز کردن یک پورت سریال، ابتدا به یک شیء SerialPort دسترسی پیدا کنید. برای این کار، می‌توانید با فراخوانی navigator.serial.requestPort() در پاسخ به یک حرکت کاربر مانند لمس یا کلیک ماوس، از کاربر بخواهید که یک پورت سریال واحد را انتخاب کند، یا یکی را از navigator.serial.getPorts() انتخاب کنید که لیستی از پورت‌های سریالی را که وب‌سایت به آنها دسترسی دارد، برمی‌گرداند.

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() یک شیء اختیاری به صورت تحت‌اللفظی می‌گیرد که فیلترها را تعریف می‌کند. این فیلترها برای تطبیق هر دستگاه سریال متصل از طریق USB با یک فروشنده USB اجباری ( usbVendorId ) و شناسه‌های محصول USB اختیاری ( 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();
تصویری از اعلان پورت سریال در یک وب‌سایت
درخواست کاربر برای انتخاب یک micro:bit بی‌بی‌سی

فراخوانی requestPort() از کاربر می‌خواهد که یک دستگاه را انتخاب کند و یک شیء SerialPort را برمی‌گرداند. هنگامی که یک شیء SerialPort دارید، فراخوانی port.open() با نرخ انتقال داده مورد نظر، پورت سریال را باز می‌کند. عضو دیکشنری baudRate مشخص می‌کند که داده‌ها با چه سرعتی از طریق خط سریال ارسال می‌شوند. این مقدار بر حسب بیت در ثانیه (bps) بیان می‌شود. برای اطلاع از مقدار صحیح، مستندات دستگاه خود را بررسی کنید زیرا اگر این مقدار به اشتباه مشخص شود، تمام داده‌هایی که ارسال و دریافت می‌کنید، نامفهوم خواهند بود. برای برخی از دستگاه‌های USB و بلوتوث که پورت سریال را شبیه‌سازی می‌کنند، این مقدار را می‌توان با خیال راحت روی هر مقداری تنظیم کرد زیرا توسط شبیه‌سازی نادیده گرفته می‌شود.

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

همچنین می‌توانید هنگام باز کردن پورت سریال، هر یک از گزینه‌های زیر را مشخص کنید. این گزینه‌ها اختیاری هستند و مقادیر پیش‌فرض مناسبی دارند.

  • dataBits : تعداد بیت‌های داده در هر فریم (۷ یا ۸).
  • stopBits : تعداد بیت‌های توقف در انتهای یک فریم (۱ یا ۲).
  • parity : حالت parity (یا "none" ، "even" یا "odd" ).
  • bufferSize : اندازه بافرهای خواندن و نوشتن که باید ایجاد شوند (باید کمتر از ۱۶ مگابایت باشد).
  • flowControl : حالت کنترل جریان (یا "none" یا "hardware" ).

خواندن از پورت سریال

جریان‌های ورودی و خروجی در Web Serial API توسط Streams API مدیریت می‌شوند.

پس از برقراری اتصال پورت سریال، ویژگی‌های readable و writable از شیء SerialPort یک ReadableStream و یک WritableStream برمی‌گردانند. این دو برای دریافت و ارسال داده از دستگاه سریال استفاده می‌شوند. هر دو از نمونه‌های Uint8Array برای انتقال داده استفاده می‌کنند.

وقتی داده‌های جدید از دستگاه سریال می‌رسد، port.readable.getReader().read() دو ویژگی را به صورت ناهمزمان برمی‌گرداند: value و یک مقدار بولی done . اگر done با true باشد، پورت سریال بسته شده است یا دیگر داده‌ای وارد نمی‌شود. فراخوانی port.readable.getReader() یک خواننده ایجاد می‌کند و readable را برای آن قفل می‌کند. در حالی که readable قفل است، پورت سریال نمی‌تواند بسته شود.

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

برخی از خطاهای خواندن پورت سریال غیرمهلک می‌توانند تحت شرایطی مانند سرریز بافر، خطاهای فریم‌بندی یا خطاهای توازن رخ دهند. این خطاها به عنوان استثنا ارسال می‌شوند و می‌توان با اضافه کردن حلقه دیگری روی حلقه قبلی که port.readable را بررسی می‌کند، آنها را دریافت کرد. این روش کار می‌کند زیرا تا زمانی که خطاها غیرمهلک باشند، یک ReadableStream جدید به طور خودکار ایجاد می‌شود. اگر خطای مهلکی مانند حذف دستگاه سریال رخ دهد، port.readable تهی می‌شود.

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

اگر دستگاه سریال متنی را برگرداند، می‌توانید port.readable از طریق یک TextDecoderStream به صورت زیر pipe کنید. TextDecoderStream یک جریان تبدیل است که تمام تکه‌های Uint8Array را می‌گیرد و آنها را به رشته تبدیل می‌کند.

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" نحوه تخصیص حافظه را هنگام خواندن از جریان کنترل کنید. برای دریافت رابط ReadableStreamBYOBReader و ارائه ArrayBuffer خود هنگام فراخوانی read() port.readable.getReader({ mode: "byob" }) را فراخوانی کنید. توجه داشته باشید که API سریال وب از این ویژگی در Chrome 106 یا بالاتر پشتیبانی می‌کند.

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 آورده شده است:

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

در اینجا مثال دیگری از نحوه خواندن مقدار مشخصی از داده از پورت سریال آورده شده است:

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

نوشتن روی پورت سریال

برای ارسال داده به یک دستگاه سریال، داده را به port.writable.getWriter().write() ارسال کنید. فراخوانی releaseLock() در port.writable.getWriter() برای بسته شدن بعدی پورت سریال ضروری است.

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

متن را از طریق یک TextEncoderStream که به port.writable متصل است، همانطور که در زیر نشان داده شده است، به دستگاه ارسال کنید.

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

const writer = textEncoder.writable.getWriter();

await writer.write("hello");

بستن پورت سریال

port.close() در صورتی که اعضای readable و writable پورت سریال قفل نباشند ، آن را می‌بندد، به این معنی که releaseLock() برای خواندن و نوشتن مربوطه فراخوانی شده است.

await port.close();

با این حال، هنگام خواندن مداوم داده‌ها از یک دستگاه سریال با استفاده از یک حلقه، port.readable همیشه قفل خواهد بود تا زمانی که با خطایی مواجه شود. در این حالت، فراخوانی reader.cancel() reader.read() مجبور می‌کند که بلافاصله با { value: undefined, done: true } حل شود و بنابراین به حلقه اجازه می‌دهد 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;
});

Closing a serial port is more complicated when using transform streams . Call reader.cancel() as before. Then call writer.close() and port.close() . This propagates errors through the transform streams to the underlying serial port. Because error propagation doesn't happen immediately, you need to use the readableStreamClosed and writableStreamClosed promises created earlier to detect when port.readable and port.writable have been unlocked. Cancelling the reader causes the stream to be aborted; this is why you must catch and ignore the resulting error.

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

گوش دادن به اتصال و قطع ارتباط

اگر یک پورت سریال توسط یک دستگاه USB ارائه شود، آن دستگاه ممکن است به سیستم متصل یا قطع شود. هنگامی که به وب‌سایت اجازه دسترسی به پورت سریال داده شد، باید رویدادهای connect و 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.
});

سیگنال‌های کنترل

پس از برقراری اتصال پورت سریال، می‌توانید سیگنال‌های در معرض پورت سریال را برای تشخیص دستگاه و کنترل جریان، به طور صریح جستجو و تنظیم کنید. این سیگنال‌ها به صورت مقادیر بولی تعریف می‌شوند. به عنوان مثال، برخی از دستگاه‌ها مانند آردوینو اگر سیگنال Data Terminal Ready (DTR) فعال باشد، وارد حالت برنامه‌نویسی می‌شوند.

تنظیم سیگنال‌های خروجی و دریافت سیگنال‌های ورودی به ترتیب با فراخوانی port.setSignals() و port.getSignals() انجام می‌شود. به مثال‌های کاربردی زیر مراجعه کنید.

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

جریان‌های دگرگون‌کننده

وقتی داده‌ها را از دستگاه سریال دریافت می‌کنید، لزوماً همه داده‌ها را یکجا دریافت نمی‌کنید. ممکن است داده‌ها به صورت دلخواه تکه‌تکه شوند. برای اطلاعات بیشتر، به مفاهیم API مربوط به Streams مراجعه کنید.

برای مقابله با این مشکل، می‌توانید از برخی جریان‌های تبدیل داخلی مانند TextDecoderStream استفاده کنید یا جریان تبدیل خودتان را ایجاد کنید که به شما امکان می‌دهد جریان ورودی را تجزیه کرده و داده‌های تجزیه‌شده را برگردانید. جریان تبدیل بین دستگاه سریال و حلقه خواندن که جریان را مصرف می‌کند، قرار می‌گیرد. این حلقه می‌تواند قبل از مصرف داده‌ها، یک تبدیل دلخواه اعمال کند. آن را مانند یک خط مونتاژ در نظر بگیرید: با پایین آمدن یک ویجت در خط، هر مرحله در خط، ویجت را تغییر می‌دهد، به طوری که تا زمانی که به مقصد نهایی خود می‌رسد، یک ویجت کاملاً کارآمد است.

عکس کارخانه هواپیماسازی
کارخانه هواپیماسازی قلعه برومویچ جنگ جهانی دوم

برای مثال، نحوه ایجاد یک کلاس جریان transform را در نظر بگیرید که یک جریان را مصرف می‌کند و آن را بر اساس شکست خط، تکه‌تکه می‌کند. متد transform() آن هر بار که داده‌های جدید توسط جریان دریافت می‌شود، فراخوانی می‌شود. این متد می‌تواند داده‌ها را در صف قرار دهد یا آنها را برای بعد ذخیره کند. متد flush() زمانی فراخوانی می‌شود که جریان بسته می‌شود و هر داده‌ای را که هنوز پردازش نشده است، مدیریت می‌کند.

برای استفاده از کلاس جریان transform، باید یک جریان ورودی را از طریق آن pipe کنید. در مثال کد سوم در بخش Read from a serial port ، جریان ورودی اصلی فقط از طریق TextDecoderStream pipe شده بود، بنابراین باید تابع pipeThrough() را برای pipe کردن آن از طریق LineBreakTransformer جدیدمان فراخوانی کنیم.

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

برای اشکال‌زدایی مشکلات ارتباط دستگاه سریال، از متد tee() در port.readable برای تقسیم جریان‌های ورودی و خروجی به دستگاه سریال استفاده کنید. دو جریان ایجاد شده می‌توانند به طور مستقل مصرف شوند و این به شما امکان می‌دهد یکی را برای بررسی در کنسول چاپ کنید.

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.

لغو دسترسی به پورت سریال

وب‌سایت می‌تواند با فراخوانی تابع forget() در نمونه‌ی SerialPort ، مجوزهای دسترسی به پورت سریالی را که دیگر علاقه‌ای به حفظ آن ندارد، پاک کند. به عنوان مثال، برای یک برنامه‌ی وب آموزشی که در یک کامپیوتر مشترک با دستگاه‌های زیاد استفاده می‌شود، تعداد زیادی از مجوزهای ایجاد شده توسط کاربر، تجربه‌ی کاربری ضعیفی را ایجاد می‌کند.

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

از آنجایی که forget() در کروم نسخه ۱۰۳ یا بالاتر موجود است، با استفاده از دستور زیر بررسی کنید که آیا این ویژگی پشتیبانی می‌شود یا خیر:

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

نکات توسعه

Debugging the Web Serial API in Chrome is easy with the internal page, about://device-log where you can see all serial device related events in one single place.

تصویری از صفحه داخلی برای اشکال‌زدایی API سریال وب.
صفحه داخلی در کروم برای اشکال‌زدایی API سریال وب.

کدلب

در آزمایشگاه کد توسعه‌دهندگان گوگل ، شما از API سریال وب برای تعامل با یک برد BBC micro:bit استفاده خواهید کرد تا تصاویر را روی ماتریس LED 5x5 آن نمایش دهید.

پشتیبانی مرورگر

رابط برنامه‌نویسی کاربردی سریال وب (Web Serial API) در تمام پلتفرم‌های دسکتاپ (ChromeOS، لینوکس، macOS و ویندوز) در کروم ۸۹ در دسترس است.

پلی‌فیل

در اندروید، پشتیبانی از پورت‌های سریال مبتنی بر USB با استفاده از WebUSB API و Serial API polyfill امکان‌پذیر است. این polyfill محدود به سخت‌افزار و پلتفرم‌هایی است که دستگاه از طریق WebUSB API قابل دسترسی است، زیرا توسط درایور دستگاه داخلی ادعا نشده است.

امنیت و حریم خصوصی

The spec authors have designed and implemented the Web Serial API using the core principles defined in Controlling Access to Powerful Web Platform Features , including user control, transparency, and ergonomics. The ability to use this API is primarily gated by a permission model that grants access to only a single serial device at a time. In response to a user prompt, the user must take active steps to select a particular serial device.

برای درک بده‌بستان‌های امنیتی، بخش‌های امنیت و حریم خصوصی Web Serial API Explainer را بررسی کنید.

بازخورد

تیم کروم دوست دارد نظرات و تجربیات شما را در مورد API سریال وب بشنود.

در مورد طراحی API به ما بگویید

آیا چیزی در مورد API وجود دارد که آنطور که انتظار می‌رود کار نمی‌کند؟ یا متدها یا ویژگی‌هایی که برای پیاده‌سازی ایده خود به آنها نیاز دارید، وجود ندارند؟

یک مشکل خاص را در مخزن Web Serial API GitHub ثبت کنید یا نظرات خود را به یک مشکل موجود اضافه کنید.

گزارش مشکل در پیاده‌سازی

آیا در پیاده‌سازی کروم اشکالی پیدا کردید؟ یا پیاده‌سازی با مشخصات متفاوت است؟

یک اشکال را در https://new.crbug.com ثبت کنید. حتماً تا حد امکان جزئیات را ذکر کنید، دستورالعمل‌های ساده‌ای برای تولید مجدد اشکال ارائه دهید و Components را روی Blink>Serial تنظیم کنید.

نمایش پشتیبانی

آیا قصد دارید از API سریال وب استفاده کنید؟ حمایت عمومی شما به تیم کروم کمک می‌کند تا ویژگی‌ها را اولویت‌بندی کنند و به سایر فروشندگان مرورگر نشان می‌دهد که پشتیبانی از آنها چقدر حیاتی است.

با استفاده از هشتگ #SerialAPI یک توییت به @ChromiumDev ارسال کنید و به ما اطلاع دهید که کجا و چگونه از آن استفاده می‌کنید.

لینک‌های مفید

دموها

تقدیرنامه‌ها

با تشکر از ریلی گرانت و جو مدلی برای نقد و بررسی‌هایشان از این سند.