גישה להתקני USB באינטרנט

ה-WebUSB API הופך את ה-USB לבטוח וקל יותר לשימוש על ידי העברתו לאינטרנט.

François Beaufort
François Beaufort

אם אמרתי פשוט ופשוט "USB", יש סיכוי גבוה תחשבו מיד על מקלדות, עכברים, מכשירי אודיו, מכשירי וידאו והתקני אחסון. היתרה שלך נכון, אבל אפשר למצוא סוגים אחרים של התקני אפיק טורי אוניברסלי (USB) שם.

התקני ה-USB הלא סטנדרטיים האלה מחייבים ספקי חומרה לכתוב שאילתות ספציפיות לפלטפורמה מנהלי התקנים וערכות SDK כדי שאתם (המפתח) יוכלו לנצל אותם. לצערנו, הקוד הספציפי לפלטפורמה הזה מנע בעבר את השימוש במכשירים האלה על ידי האינטרנט. זו אחת מהסיבות ליצירת ה-WebUSB API: מספקות דרך לחשוף שירותי התקן USB לאינטרנט. באמצעות ה-API הזה, החומרה יצרנים יוכלו ליצור ערכות JavaScript SDK חוצות פלטפורמות מכשירים.

אבל הדבר החשוב ביותר הוא שהפעולה תהפוך את ה-USB לבטוח וקל יותר לשימוש אותו לאינטרנט.

אלו הן ההתנהגות הצפויה ב-WebUSB API:

  1. קונים התקן USB.
  2. מחברים אותו למחשב. התראה תופיע מיד, בצד שמאל. אל המכשיר הזה.
  3. לוחצים על ההודעה. האתר כבר שם ומוכן לשימוש.
  4. יש ללחוץ כדי להתחבר ובורר התקני USB מופיע ב-Chrome, שם אפשר בוחרים את המכשיר הרצוי.

הנה!

איך היה נראה התהליך ללא WebUSB API?

  1. התקנת אפליקציה ספציפית לפלטפורמה.
  2. אם היא אפילו נתמכת במערכת ההפעלה שלי, ודא שהורדתי כי הן צודקות.
  3. מתקינים את הפריט. אם קצת מזלך, לא יוצגו לך הנחיות מפחידות או חלונות קופצים של מערכת ההפעלה. להזהיר אתכם לגבי התקנת מנהלי התקנים/אפליקציות מהאינטרנט. אם המיקום שיש לך מזל, מנהלי ההתקנים או האפליקציות המותקנים מתקשים וגורמים נזק במחשב שלך. (חשוב לזכור שהאינטרנט נועד לכלול תקלות אתרים).
  4. אם משתמשים בתכונה רק פעם אחת, הקוד נשאר במחשב עד כדאי להסיר אותו. (באינטרנט, בסופו של דבר המקום שלא בשימוש reclaimed.)

לפני שמתחילים

מאמר זה מבוסס על ההנחה שיש לכם ידע בסיסי על אופן הפעולה של USB. אם לא, מומלץ לקרוא את המאמר USB in a NutShell. לקבלת מידע רקע על USB, כדאי לעיין במפרט הרשמי של USB.

WebUSB API זמין ב-Chrome 61.

זמין לגרסאות מקור לניסיון

כדי לקבל משוב רב ככל האפשר ממפתחים שמשתמשים ב-WebUSB API בשדה, הוספנו את התכונה הזו בעבר ב-Chrome 54 וב-Chrome 57 כגרסת מקור לניסיון.

תקופת הניסיון האחרונה הסתיימה בהצלחה בספטמבר 2017.

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

רק HTTPS

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

נדרשת תנועת משתמש

מטעמי אבטחה, navigator.usb.requestDevice() יכול/ה רק לקבל קריאה באמצעות תנועת משתמש, כמו מגע או לחיצה בעכבר.

מדיניות ההרשאות

מדיניות הרשאות היא מנגנון שמאפשר למפתחים להפעיל באופן סלקטיבי ולהשבית תכונות דפדפן שונות וממשקי API שונים. אפשר להגדיר אותו באמצעות HTTP כותרת ו/או "allow" ב-iframe .

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

הנה דוגמה למדיניות כותרת שבה אסור להשתמש ב-WebUSB:

Feature-Policy: fullscreen "*"; usb "none"; payment "self" https://payment.example.com

דוגמה נוספת למדיניות מאגר תגים שבה מותר להשתמש ב-USB:

<iframe allowpaymentrequest allow="usb; fullscreen"></iframe>

שנתחיל לתכנת?

ה-WebUSB API מסתמך במידה רבה על הבטחות של JavaScript. אם אתם לא מכירים איתם, כדאי לצפות במדריך הנהדר הזה בנושא Promises. עוד דבר אחד, () => {} הן פשוט פונקציות חיצים של ECMAScript 2015.

קבלת גישה להתקני USB

אפשר לבקש מהמשתמש לבחור התקן USB אחד מחובר באמצעות navigator.usb.requestDevice() או להתקשר למספר navigator.usb.getDevices() כדי לקבל רשימה של כל התקני ה-USB המחוברים שהאתר קיבל גישה אליהם.

הפונקציה navigator.usb.requestDevice() לוקחת אובייקט JavaScript חובה שמגדירה את filters. המסננים האלה משמשים להתאמה לכל התקני USB עם נתון (vendorId) של ספק, ובאופן אופציונלי גם מזהי מוצר (productId). המפתחות classCode, protocolCode, serialNumber וגם subclassCode יכולים להגדיר גם שם.

צילום מסך של הנחיה מהמשתמש להתקן USB ב-Chrome
הודעת משתמש למכשיר USB.

לדוגמה, כך אפשר לקבל גישה למכשיר Arduino מחובר כדי לאפשר את המקור.

navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
.then(device => {
  console.log(device.productName);      // "Arduino Micro"
  console.log(device.manufacturerName); // "Arduino LLC"
})
.catch(error => { console.error(error); });

לפני שמבקשים, לא המציאתי קסם את ההקסדצימלי הזה של 0x2341 מספר. חיפשתי את המילה "Arduino" ברשימה של מזהי ה-USB.

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

// Get all connected USB devices the website has been granted access to.
navigator.usb.getDevices().then(devices => {
  devices.forEach(device => {
    console.log(device.productName);      // "Arduino Micro"
    console.log(device.manufacturerName); // "Arduino LLC"
  });
})

דרך אגב, אם התקן USB מכריז על תמיכה ב-WebUSB, וכן שמגדירים כתובת אתר של דף נחיתה, Chrome יציג התראה קבועה כאשר התקן USB מחובר. לחיצה על ההודעה הזו תפתח את דף הנחיתה.

צילום מסך של התראת WebUSB ב-Chrome
התראת WebUSB.

דיבור אל לוח USB של Arduino

אוקיי, עכשיו נראה כמה קל לתקשר בעזרת WebUSB לוח Arduino מעל יציאת ה-USB. אפשר לעיין בהוראות בכתובת https://github.com/webusb/arduino ל-WebUSB-להפעיל שרטוטים.

אל דאגה, אני אסקור את כל השיטות של התקן WebUSB המצוינות בהמשך בשלב מאוחר יותר במאמר הזה.

let device;

navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
.then(selectedDevice => {
    device = selectedDevice;
    return device.open(); // Begin a session.
  })
.then(() => device.selectConfiguration(1)) // Select configuration #1 for the device.
.then(() => device.claimInterface(2)) // Request exclusive control over interface #2.
.then(() => device.controlTransferOut({
    requestType: 'class',
    recipient: 'interface',
    request: 0x22,
    value: 0x01,
    index: 0x02})) // Ready to receive data
.then(() => device.transferIn(5, 64)) // Waiting for 64 bytes of data from endpoint #5.
.then(result => {
  const decoder = new TextDecoder();
  console.log('Received: ' + decoder.decode(result.data));
})
.catch(error => { console.error(error); });

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

הנה השרטוט שהועלה ללוח של Arduino.

// Third-party WebUSB Arduino library
#include <WebUSB.h>

WebUSB WebUSBSerial(1 /* https:// */, "webusb.github.io/arduino/demos");

#define Serial WebUSBSerial

void setup() {
  Serial.begin(9600);
  while (!Serial) {
    ; // Wait for serial port to connect.
  }
  Serial.write("WebUSB FTW!");
  Serial.flush();
}

void loop() {
  // Nothing here for now.
}

ספריית WebUSB Arduino של צד שלישי שבה נעשה שימוש בקוד לדוגמה שלמעלה בעיקרון שני דברים:

  • המכשיר פועל כמכשיר WebUSB שמאפשר ל-Chrome לקרוא את כתובת דף הנחיתה.
  • היא חושפת ממשק API טורי WebUSB שאפשר להשתמש בו כדי לשנות את ברירת המחדל.

בודקים שוב את קוד ה-JavaScript. אחרי שאקבל את device שנבחר על ידי המשתמש, מערכת device.open() מריצה את כל השלבים הספציפיים לפלטפורמה כדי להתחיל סשן עם ה-USB במכשיר. לאחר מכן צריך לבחור תצורת USB זמינה עם device.selectConfiguration() חשוב לזכור שתצורה מציינת איך שהוא מופעל, צריכת החשמל המקסימלית שלו ומספר הממשקים שלו. ואם כבר מדברים על ממשקים, עליי לבקש גישה בלעדית עם device.claimInterface() מכיוון שניתן להעביר נתונים רק לממשק או נקודות הקצה המשויכות כשנתבעה בעלות על הממשק. סוף סוף אנחנו מתקשרים יש צורך ב-device.controlTransferOut() כדי להגדיר את מכשיר Arduino עם את הפקודות המתאימות כדי לקיים תקשורת באמצעות WebUSB Series API.

משם, device.transferIn() מבצע העברה בכמות גדולה אל המכשיר כדי להודיע לו שהמארח מוכן לקבל נתונים בכמות גדולה. לאחר מכן, ההבטחה ממומשת באמצעות אובייקט result שמכיל data ב-DataView שצריך לנתח בצורה נכונה.

אם אתם מכירים את ה-USB, כל הציוד הזה אמור להיראות לכם די מוכר.

אני רוצה עוד

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

  • העברות מסוג Control, שמשמשות לשליחה או לקבלה של הגדרות אישיות או פקודות להתקן USB. אלו הפרמטרים controlTransferIn(setup, length) ו-controlTransferOut(setup, data).
  • העברות INTERRUPT, משמשות למשך פרק זמן קצר של מידע אישי רגיש מטופלות באותן שיטות כמו העברות BULK עם transferIn(endpointNumber, length) וגם transferOut(endpointNumber, data)
  • העברות ISOCHRONOUS שמשמשות לסטרימינג של נתונים כגון וידאו וקול, טופל עם isochronousTransferIn(endpointNumber, packetLengths) ועם isochronousTransferOut(endpointNumber, data, packetLengths).
  • העברות BULK, המשמשות להעברת כמות גדולה של מידע לא רגיש בזמן באופן אמין, מטופלות עם transferIn(endpointNumber, length) transferOut(endpointNumber, data).

כדאי גם להציץ בפרויקט WebLight של מייק טסאו. מספק דוגמה בסיסית לבניית מכשיר LED עם בקרת USB, שנועד ל-WebUSB API (לא משתמשים כאן ב-Arduino). אפשר למצוא בה חומרה, תוכנה, וקושחה.

שלילת הגישה להתקן USB

האתר יכול לנקות את הרשאות הגישה להתקן USB שכבר אין לו צורך באמצעות הפקודה forget() במכונה USBDevice. לדוגמה, עבור יישום אינטרנט חינוכי, המשמש במחשב משותף עם מכשירים רבים, מספר ההרשאות המצטברות שנוצרו על ידי המשתמשים, פוגע בחוויית המשתמש.

// Voluntarily revoke access to this USB device.
await device.forget();

forget() זמין בגרסה 101 ואילך של Chrome, לכן כדאי לבדוק אם התכונה הזו נתמכים עם:

if ("usb" in navigator && "forget" in USBDevice.prototype) {
  // forget() is supported.
}

מגבלות על גודל ההעברה

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

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

const BULK_TRANSFER_SIZE = 16 * 1024; // 16KB
const MAX_NUMBER_TRANSFERS = 3;

async function sendRawPayload(device, endpointNumber, data) {
  let i = 0;
  let pendingTransfers = [];
  let remainingBytes = data.byteLength;
  while (remainingBytes > 0) {
    const chunk = data.subarray(
      i * BULK_TRANSFER_SIZE,
      (i + 1) * BULK_TRANSFER_SIZE
    );
    // If we've reached max number of transfers, let's wait.
    if (pendingTransfers.length == MAX_NUMBER_TRANSFERS) {
      await pendingTransfers.shift();
    }
    // Submit transfers that will be executed in order.
    pendingTransfers.push(device.transferOut(endpointNumber, chunk));
    remainingBytes -= chunk.byteLength;
    i++;
  }
  // And wait for last remaining transfers to complete.
  await Promise.all(pendingTransfers);
}

טיפים

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

צילום מסך של דף היומן של המכשיר לצורך ניפוי באגים ב-WebUSB ב-Chrome
דף היומן של המכשיר ב-Chrome לניפוי באגים ב-WebUSB API.

גם הדף הפנימי about://usb-internals מועיל ומאפשר לך כדי לדמות חיבור וניתוק של מכשירי WebUSB וירטואליים. האפשרות הזו שימושית כשרוצים לבדוק ממשק משתמש בלי חומרה אמיתית.

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

ברוב מערכות Linux, התקני USB ממופים עם הרשאות קריאה בלבד על ידי כברירת מחדל. כדי לאפשר ל-Chrome לפתוח התקן USB, צריך להוסיף udev חדש כלל. יצירת קובץ ב-/etc/udev/rules.d/50-yourdevicename.rules עם התוכן הבא:

SUBSYSTEM=="usb", ATTR{idVendor}=="[yourdevicevendor]", MODE="0664", GROUP="plugdev"

כאשר [yourdevicevendor] הוא 2341, למשל אם המכשיר הוא Arduino. אפשר גם להוסיף את ATTR{idProduct} לכלל ספציפי יותר. מוודאים שהמאפיינים user הוא חבר בקבוצה plugdev. אחר כך פשוט מחברים מחדש את המכשיר.

משאבים

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

אישורים

תודה לג'ו מדלי על סקירת המאמר הזה.