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

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

François Beaufort
François Beaufort

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

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

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

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

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

וואו!

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

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