קריאה וכתיבה מיציאה טורית

Web Series API מאפשר לאתרים לתקשר עם מכשירים עם יציאה טורית.

François Beaufort
François Beaufort

מה זה Web Series API?

יציאה טורית היא ממשק תקשורת דו-כיווני שמאפשר שליחה מקבל נתונים בייט אחר בייט.

ה-Web Series API מאפשר לאתרים לקרוא ולכתוב ב- מכשיר עם יציאה טורית באמצעות JavaScript. מכשירים עם יציאה טורית מחוברים באמצעות יציאה טורית במערכת המשתמש או באמצעות התקני USB ו-Bluetooth נשלפים שאמולציה של יציאה טורית.

במילים אחרות, ה-Web Series API מגשר בין האינטרנט לעולם הפיזי על ידי מתן אפשרות לאתרים לתקשר עם מכשירים עם יציאה טורית, כמו מיקרו-בקרים ומדפסות תלת ממד.

ה-API הזה גם עוזר מאוד להשתמש ב-WebUSB כי מערכות ההפעלה דורשות לתקשורת עם יציאות טוריות מסוימות באמצעות הרמה הגבוהה יותר סידורי API במקום API ברמה נמוכה ל-USB.

הצעות לתרחישים לדוגמה

במגזרים החינוכיים, החובבים והתעשייתיים, משתמשים מחברים ציוד היקפי. למחשבים שלהם. לרוב, המכשירים האלה נשלטים על ידי מיקרו-בקרים באמצעות חיבור טורי שמשמש את תוכנה מותאמת אישית. כמה פריטים מותאמים אישית תוכנה לשליטה במכשירים אלה נוצרה באמצעות טכנולוגיית אינטרנט:

במקרים מסוימים, אתרים מתקשרים עם המכשיר באמצעות נציג אפליקציה שהמשתמשים התקינו באופן ידני. במקרים אחרים, האפליקציה שמסופקת באפליקציה ארוזה באמצעות framework כמו Electron. במקרים אחרים המשתמש נדרש לבצע שלב נוסף כמו העתקת אפליקציה שעברה הידור למכשיר באמצעות כונן USB נייד.

בכל המקרים האלה, תשפר את חוויית המשתמש על ידי מתן גישה ישירה תקשורת בין האתר למכשיר שבו הוא שולט.

הסטטוס הנוכחי

שלב סטטוס
1. יצירת הסבר הושלם
2. יצירת טיוטה ראשונית של מפרט הושלם
3. איסוף משוב לבצע איטרציה בעיצוב הושלם
4. גרסת מקור לניסיון הושלם
5. הפעלה הושלם

שימוש ב-Web Series API

זיהוי תכונות

כדי לבדוק אם יש תמיכה ב-Web Series API, משתמשים בישויות הבאות:

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

פתיחת יציאה טורית

ה-Web Series 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();
צילום מסך של הנחיה ליציאה טורית באתר
בקשת משתמש לבחירת מיקרו:ביט של BBC

התקשרות אל requestPort() מבקשת מהמשתמש לבחור מכשיר ומחזירה אובייקט SerialPort. ברגע שיש אובייקט SerialPort, קוראים לפונקציה port.open() בקצב הבאוד הרצוי, יפתח את היציאה הטורית. המילון של baudRate רכיב שמציין את מהירות שליחת הנתונים באמצעות שורה טורית. בא לידי ביטוי ב יחידות של ביטים לשנייה (bps). מחפשים את מסמכי התיעוד של המכשיר. בערך הנכון, כי כל הנתונים שתשלחו ותקבלו יהיו ג'יבריש. צוין באופן שגוי. במכשירי USB ו-Bluetooth מסוימים שאמולציה של מספר סידורי או להקצות את הערך הזה בבטחה לכל ערך, מכיוון שהמערכת מתעלמת ממנו אמולציה.

// 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: מספר הביטים של הנתונים לכל מסגרת (7 או 8).
  • stopBits: מספר הביטים של העצירה בסוף פריים (1 או 2).
  • parity: מצב ההתאמה ("none", "even" או "odd").
  • bufferSize: הגודל של מאגרי הנתונים הזמניים לקריאה וכתיבה שצריך ליצור (חייב להיות קטן מ-16MB).
  • flowControl: מצב בקרת הזרימה ("none" או "hardware").

קריאה מיציאה טורית

שידורי הקלט והפלט ב-Web Series 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 הופך null.

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 כמו שמוצג בהמשך. 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' בקורא. אפשר להתקשר אל port.readable.getReader({ mode: "byob" }) כדי לקבל את הממשק ReadableStreamBYOBReader ולספק את התוכן של ArrayBuffer לביצוע קריאה ל-read(). לתשומת ליבכם: Web Series 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;
});

סגירת יציאה טורית מורכבת יותר כשמשתמשים בטרנספורמציה של שידורים חיים. להתקשר אל reader.cancel() כמו קודם. לאחר מכן צריך להתקשר אל writer.close() ולport.close(). הפעולה הזו גורמת לשגיאות את הטרנספורמציה של הסטרימינג ליציאה הטורית הבסיסית. כי הפצת שגיאות לא מתבצע באופן מיידי, עליך להשתמש בreadableStreamClosed writableStreamClosed הבטחות שנוצרו מוקדם יותר כדי לזהות מתי port.readable ו הנעילה של port.writable בוטלה. ביטול של reader גורם כדי לבטל זרם לכן צריך לזהות את השגיאה ולתעלם ממנה.

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

כינויים של אותות

לאחר יצירת החיבור ליציאה הטורית, ניתן להריץ שאילתה ולהגדיר באופן מפורש אותות שנחשפים על ידי היציאה הטורית לזיהוי המכשיר ולבקרת הזרימה. האלה אותות מוגדרים כערכים בוליאניים. לדוגמה, מכשירים מסוימים כמו Arduino יעבור למצב תכנות אם האות 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 של סטרימינג.

כדי להתמודד עם הבעיה הזו, אפשר להשתמש בכמה שידורי טרנספורמציה מובנים כמו TextDecoderStream או ליצור זרם טרנספורמציה משלך שמאפשר: לנתח את הזרם הנכנס ולהחזיר נתונים מנותחים. זרם הטרנספורמציה יושב בין המכשיר הסידורי לבין לולאת הקריאה שצורכת את השידור. אפשר להחיל טרנספורמציה שרירותית לפני צריכת הנתונים. צריך לחשוב על זה כמו על שורת הרכבה: כאשר ווידג'ט מגיע לשורה, כל שלב בשורה משתנה את הווידג'ט, כך שעד שהוא מגיע ליעד הסופי שלו, הוא של ווידג'ט תקין.

תמונה של מפעל לייצור מטוסים
מפעל תעופה במטוסים של טירת ברומוויץ' ממלחמת העולם השנייה

לדוגמה, נבחן את האופן שבו ניתן ליצור מחלקה של זרם טרנספורמציה שצורכת לשדר ולחלק אותו לפי מעברי שורה. ה-method שלה transform() נקראת בכל פעם שהזרם מקבל נתונים חדשים. הוא יכול להעביר את הנתונים לתור או לשמור אותה לשימוש במועד מאוחר יותר. מתבצעת קריאה לשיטה flush() כשהשידור סגור. הוא מטפל בנתונים שעדיין לא עברו עיבוד.

כדי להשתמש במחלקה של טרנספורמציה של זרם, צריך להעביר זרם נכנס דרך את זה. בדוגמת הקוד השלישית שמופיעה בקטע קריאה מיציאה טורית, זרם הקלט המקורי הועבר רק דרך TextDecoderStream, כך צריכים להתקשר אל pipeThrough() כדי להעביר אותו דרך 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() זמין בגרסה 103 ואילך של Chrome, לכן כדאי לבדוק אם התכונה הזו נתמכים עם:

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

טיפים למפתחים

קל לנפות באגים ב-Web Series API ב-Chrome בעזרת הדף הפנימי, about://device-log שבו אפשר לראות את כל האירועים שקשורים למכשיר עם יציאה טורית באירוע אחד במקום אחד.

צילום מסך של הדף הפנימי לניפוי באגים ב-Web Series API.
דף פנימי ב-Chrome לניפוי באגים ב-Web Series API.

Codelab

ב-Google Developer Codelab, משתמשים ב-Web Series API כדי לקיים אינטראקציה עם לוח micro:bit כדי להציג תמונות במטריצת LED בגודל 5x5.

תמיכה בדפדפנים

ה-Web Series API זמין בכל פלטפורמות המחשב (ChromeOS, Linux, macOS ו-Windows) ב-Chrome 89.

פוליפיל

ב-Android, אפשר להשתמש ביציאות טוריות מבוססות-USB באמצעות WebUSB API ו-polyfill של Series API. ה-polyfill הזה מוגבל לחומרה ול פלטפורמות שבהן ניתן לגשת אל המכשיר דרך WebUSB API כי נדרשה בעלות על ידי מנהל התקן מובנה.

אבטחה ופרטיות

מחברי המפרט תכננו והטמיעו את Web Series API באמצעות הליבה את העקרונות שמוגדרים בשליטה בגישה לתכונות חזקות של פלטפורמת האינטרנט, כולל שליטה של משתמשים, שקיפות וארגונומיה. היכולת להשתמש ה-API מוגבל בעיקר על ידי מודל הרשאות שמעניק גישה רק של המכשיר עם יציאה טורית בכל פעם. בתגובה לבקשה מהמשתמש, המשתמש צריך להיות פעיל השלבים לבחירת מכשיר עם יציאה טורית מסוימת.

כדי להבין את יתרונות האבטחה, כדאי לעיין באבטחה ובפרטיות בהסבר על Web Series API.

משוב

צוות Chrome ישמח לשמוע על המחשבות והחוויות שלך עם Web Series API.

מתארים את עיצוב ה-API

האם יש משהו ב-API שלא פועל כצפוי? או שיש חסרות שיטות או מאפיינים שאתם צריכים ליישם את הרעיון שלכם?

מגישים בעיית מפרט במאגר GitHub של Web Series API או מוסיפים מחשבות על בעיה קיימת.

דיווח על בעיה בהטמעה

מצאת באג בהטמעה של Chrome? או שההטמעה שונה מהמפרט?

דווחו על באג בכתובת https://new.crbug.com. הקפידו לכלול כמה שיותר לספק הוראות פשוטות לשחזור הבאג, הרכיבים הוגדרו ל-Blink>Serial. גליץ' הוא פתרון מעולה ולשתף תגובות מהירות וקלות.

פנייה לתמיכה

האם בכוונתך להשתמש ב-Web Series API? התמיכה הציבורית שלך עוזרת ל-Chrome הצוות מתעדף תכונות ומראה לספקי דפדפנים אחרים עד כמה זה קריטי לתמוך בהם.

שליחת ציוץ אל @ChromiumDev בעזרת hashtag #SerialAPI וספר לנו איפה אתה משתמש בו ובאיזה אופן.

קישורים שימושיים

הדגמות

אישורים

תודה לReilly Grant ולJoe Medley שהביקורות שלהם על המאמר הזה. תמונה של מפעל מטוסים בשיתוף עם קרן המוזיאונים של ברמינגהם בערוץ Un פעילות.