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

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

François Beaufort
François Beaufort

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

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

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

בואו נראה את ההתנהגות הצפויה עם WebUSB API:

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

הנה!

כיצד יתבצע התהליך ללא WebUSB API?

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

לפני שאתחיל

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

WebUSB API זמין ב-Chrome 61.

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

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

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

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

רק HTTPS

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

אוקיי, עכשיו נראה כמה קל לתקשר מלוח Arduino תואם ל-WebUSB דרך יציאת ה-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 לקרוא את כתובת דף הנחיתה.
  • הוא חושף ממשק 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).
  • העברות ISOCHRONOUS המשמשות לשידורים של נתונים כמו וידאו וקול מטופלות באמצעות 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 באמצעות ה-hashtag #WebUSB וספר לנו איפה אתם משתמשים בו ואיך אתם משתמשים בו.

אישורים

תודה על הביקורת של Joe Medley.