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

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

François Beaufort
François Beaufort

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

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

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

נראה מהי ההתנהגות הצפויה ב-WebUSB API:

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

וואו!

איך התהליך הזה ייראה בלי WebUSB API?

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

לפני שאתחיל

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

WebUSB API זמין ב-Chrome 61.

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

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

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

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

רק HTTPS

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

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

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

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

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

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

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

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

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

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

מתחילים לתכנת

ממשק ה-WebUSB API מסתמך במידה רבה על Promises של JavaScript. אם אתם לא מכירים אותם, כדאי לעיין במדריך המצוין הזה ל-Promises. דבר נוסף: () => {} הם פשוט פונקציות Arrow של 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

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

אל דאגה, אעסוק בכל השיטות של מכשירי 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 לקרוא את כתובת ה-URL של דף הנחיתה.
  • היא חושפת ממשק API טורי WebUSB שבו ניתן להשתמש כדי לשנות את ברירת המחדל.

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

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

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

אני רוצה עוד

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

  • העברות בקרה, שמשמשות לשליחה או לקבלה של פרמטרים של הגדרות או פקודות למכשיר USB, מטופלות באמצעות controlTransferIn(setup, length) ו-controlTransferOut(setup, data).
  • העברות INTERRUPT, שמשמשות לפרק זמן קצר של מידע אישי רגיש, מטופלות באותן שיטות כמו העברות BULK באמצעות transferIn(endpointNumber, length) ו-transferOut(endpointNumber, data).
  • העברות איזוכרוניות, שמשמשות למקורות נתונים כמו וידאו וצליל, מטופלות באמצעות 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() זמינה ב-Chrome בגרסה 101 ואילך, לכן צריך לבדוק אם היא נתמכת:

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 באמצעות ההאשטאג #WebUSB ולספר לנו איפה ואיך אתם משתמשים בו.

תודות

תודה ל-Joe Medley על בדיקת המאמר הזה.