התחברות למכשירי HID לא נפוצים

ה-WebHID API מאפשר לאתרים לגשת למקלדות עזר חלופיות ולגאימפדים אקזוטיים.

François Beaufort
François Beaufort

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

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

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

חוסר היכולת לגשת למכשירי HID לא נפוצים כואב במיוחד כשמדובר במקלדות עזר חלופיות (למשל Elgato Stream Deck, אוזניות Jabra, X-keys) ותמיכה במכשירי גיימפאד אקזוטיים. בקר משחקים שמיועד למחשבים נייחים משתמש לעיתים קרובות ב-HID להזנות (לחצנים, ג’ויסטיקים, טריגרים) ולפלט (נורות LED, רעידות). לצערנו, הקלט והפלט של גיימפאד לא סטנדרטיים, ולרוב דפדפני אינטרנט דורשים לוגיקה מותאמת אישית למכשירים ספציפיים. זוהי שיטה לא בת קיימא, והיא גורמת לתמיכה חלשה בחלק הארי של המכשירים הישנים והלא נפוצים. בנוסף, היא גורמת לדפדפן להסתמך על תכונות חריגות בהתנהגות של מכשירים ספציפיים.

הסברים על המונחים

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

מכשיר HID (Human Interface Device) הוא סוג של מכשיר שמקבל קלט מבני אדם או מספק להם פלט. הוא מתייחס גם לפרוטוקול HID, תקן לתקשורת דו-כיוונית בין מארח למכשיר, שנועד לפשט את תהליך ההתקנה. פרוטוקול ה-HID פותח במקור למכשירי USB, אבל מאז הוא יושם בפרוטוקולים רבים אחרים, כולל Bluetooth.

אפליקציות ומכשירי HID מחליפים נתונים בינאריים באמצעות שלושה סוגי דוחות:

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

מתאר הדוח מתאר את הפורמט הבינארי של הדוחות שנתמכים במכשיר. המבנה שלו היררכי, וניתן לקבץ דוחות יחד כאוספים נפרדים בתוך האוסף ברמה העליונה. הפורמט של המתאר מוגדר במפרט HID.

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

שימוש ב-WebHID API

זיהוי תכונות

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

if ("hid" in navigator) {
  // The WebHID API is supported.
}

פתיחת חיבור HID

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

כדי לפתוח חיבור HID, קודם צריך לגשת לאובייקט HIDDevice. לשם כך, אפשר לבקש מהמשתמש לבחור מכשיר באמצעות קריאה ל-navigator.hid.requestDevice(), או לבחור מכשיר מתוך navigator.hid.getDevices() שמחזיר רשימה של מכשירים שהאתר קיבל גישה אליהם בעבר.

הפונקציה navigator.hid.requestDevice() מקבלת אובייקט חובה שמגדיר מסננים. הם משמשים להתאמה של כל מכשיר שמחובר באמצעות מזהה של ספק USB‏ (vendorId), מזהה של מוצר USB‏ (productId), ערך של דף שימוש (usagePage) וערך שימוש (usage). אפשר לקבל אותם ממאגר המזהים של USB ומהמסמך של טבלאות השימוש של HID.

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

// Filter on devices with the Nintendo Switch Joy-Con USB Vendor/Product IDs.
const filters = [
  {
    vendorId: 0x057e, // Nintendo Co., Ltd
    productId: 0x2006 // Joy-Con Left
  },
  {
    vendorId: 0x057e, // Nintendo Co., Ltd
    productId: 0x2007 // Joy-Con Right
  }
];

// Prompt user to select a Joy-Con device.
const [device] = await navigator.hid.requestDevice({ filters });
// Get all devices the user has previously granted the website access to.
const devices = await navigator.hid.getDevices();
צילום מסך של הנחיה לגבי מכשיר HID באתר.
הנחיה למשתמש לבחירת Nintendo Switch Joy-Con.

אפשר גם להשתמש במקש האופציונלי exclusionFilters ב-navigator.hid.requestDevice() כדי להחריג מבורר הדפדפנים מכשירים מסוימים, למשל מכשירים שידועים כפגומים.

// Request access to a device with vendor ID 0xABCD. The device must also have
// a collection with usage page Consumer (0x000C) and usage ID Consumer
// Control (0x0001). The device with product ID 0x1234 is malfunctioning.
const [device] = await navigator.hid.requestDevice({
  filters: [{ vendorId: 0xabcd, usagePage: 0x000c, usage: 0x0001 }],
  exclusionFilters: [{ vendorId: 0xabcd, productId: 0x1234 }],
});

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

for (let collection of device.collections) {
  // An HID collection includes usage, usage page, reports, and subcollections.
  console.log(`Usage: ${collection.usage}`);
  console.log(`Usage page: ${collection.usagePage}`);

  for (let inputReport of collection.inputReports) {
    console.log(`Input report: ${inputReport.reportId}`);
    // Loop through inputReport.items
  }

  for (let outputReport of collection.outputReports) {
    console.log(`Output report: ${outputReport.reportId}`);
    // Loop through outputReport.items
  }

  for (let featureReport of collection.featureReports) {
    console.log(`Feature report: ${featureReport.reportId}`);
    // Loop through featureReport.items
  }

  // Loop through subcollections with collection.children
}

כברירת מחדל, מכשירי HIDDevice מוחזרים במצב 'סגור', וצריך לקרוא ל-open() כדי לפתוח אותם לפני שאפשר לשלוח או לקבל נתונים.

// Wait for the HID connection to open before sending/receiving data.
await device.open();

קבלת דוחות קלט

אחרי יצירת החיבור ל-HID, אפשר לטפל בדוחות הקלט הנכנסים על ידי האזנה לאירועים "inputreport" מהמכשיר. האירועים האלה מכילים את נתוני ה-HID כאובייקט DataView (data), את מכשיר ה-HID שאליו הם שייכים (device) ואת מזהה הדוח בן 8 הביטים שמשויך לדוח הקלט (reportId).

תמונה של Nintendo Switch בצבע אדום וכחול.
מכשירי Nintendo Switch Joy-Con.

בהמשך לדוגמה הקודמת, הקוד שבהמשך מראה איך לזהות את הלחצן שהמשתמש לחץ עליו במכשיר Joy-Con Right, כדי שתוכלו לנסות את זה בבית.

device.addEventListener("inputreport", event => {
  const { data, device, reportId } = event;

  // Handle only the Joy-Con Right device and a specific report ID.
  if (device.productId !== 0x2007 && reportId !== 0x3f) return;

  const value = data.getUint8(0);
  if (value === 0) return;

  const someButtons = { 1: "A", 2: "X", 4: "B", 8: "Y" };
  console.log(`User pressed button ${someButtons[value]}.`);
});

שליחת דוחות פלט

כדי לשלוח דוח פלט למכשיר HID, מעבירים את מזהה הדוח בן 8 הביט שמשויך לדוח הפלט (reportId) ואת הבייטים כ-BufferSource (data) אל device.sendReport(). ההבטחה שתוחזר תבוטל אחרי שהדוח יישלח. אם מכשיר ה-HID לא משתמש במזהי דוחות, מגדירים את reportId כ-0.

הדוגמה הבאה רלוונטית למכשיר Joy-Con ומראה איך לגרום לו לרטוט באמצעות דוחות פלט.

// First, send a command to enable vibration.
// Magical bytes come from https://github.com/mzyy94/joycon-toolweb
const enableVibrationData = [1, 0, 1, 64, 64, 0, 1, 64, 64, 0x48, 0x01];
await device.sendReport(0x01, new Uint8Array(enableVibrationData));

// Then, send a command to make the Joy-Con device rumble.
// Actual bytes are available in the sample below.
const rumbleData = [ /* ... */ ];
await device.sendReport(0x10, new Uint8Array(rumbleData));

שליחה וקבלה של דוחות על תכונות

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

תמונה של מחשב נייד שחור-כסוף.
מקלדת של מחשב נייד

כדי לשלוח דוח תכונות למכשיר HID, מעבירים את מזהה הדוח בן 8 הביט שמשויך לדוח התכונות (reportId) ואת הבייטים כ-BufferSource (data) אל device.sendFeatureReport(). ההבטחה שתוחזר תבוטל אחרי שהדוח יישלח. אם מכשיר ה-HID לא משתמש במזהי דוחות, מגדירים את reportId כ-0.

בדוגמה הבאה מוסבר איך להשתמש בדוחות תכונות, באמצעות בקשה למכשיר עם תאורת מקלדת של Apple, פתיחת המכשיר והפעלת הבהוב.

const waitFor = duration => new Promise(r => setTimeout(r, duration));

// Prompt user to select an Apple Keyboard Backlight device.
const [device] = await navigator.hid.requestDevice({
  filters: [{ vendorId: 0x05ac, usage: 0x0f, usagePage: 0xff00 }]
});

// Wait for the HID connection to open.
await device.open();

// Blink!
const reportId = 1;
for (let i = 0; i < 10; i++) {
  // Turn off
  await device.sendFeatureReport(reportId, Uint32Array.from([0, 0]));
  await waitFor(100);
  // Turn on
  await device.sendFeatureReport(reportId, Uint32Array.from([512, 0]));
  await waitFor(100);
}

כדי לקבל דוח תכונות ממכשיר HID, מעבירים את מזהה הדוח בן 8 הביט שמשויך לדוח התכונות (reportId) אל device.receiveFeatureReport(). ההבטחה שתוחזר תיפתר עם אובייקט DataView שמכיל את התוכן של דוח התכונות. אם במכשיר ה-HID לא נעשה שימוש במזהי דוחות, מגדירים את reportId לערך 0.

// Request feature report.
const dataView = await device.receiveFeatureReport(/* reportId= */ 1);

// Read feature report contents with dataView.getInt8(), getUint8(), etc...

האזנה לחיבור ולניתוק

כשהאתר קיבל הרשאה לגשת להתקן HID, הוא יכול לקבל באופן פעיל אירועי חיבור וניתוק על ידי האזנה לאירועים "connect" ו-"disconnect".

navigator.hid.addEventListener("connect", event => {
  // Automatically open event.device or warn user a device is available.
});

navigator.hid.addEventListener("disconnect", event => {
  // Remove |event.device| from the UI.
});

ביטול הגישה למכשיר HID

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

קריאה ל-forget() במכונה אחת של HIDDevice תבוטל את הגישה לכל ממשקי ה-HID באותו מכשיר פיזי.

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

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

if ("hid" in navigator && "forget" in HIDDevice.prototype) {
  // forget() is supported.
}

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

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

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

כדאי להיעזר ב-HID explorer כדי להעביר את המידע של מכשירי HID לפורמט קריא לבני אדם. הוא ממפה ערכים של שימוש לשמות לכל שימוש ב-HID.

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

KERNEL=="hidraw*", ATTRS{idVendor}=="[yourdevicevendor]", MODE="0664", GROUP="plugdev"

בשורה שלמעלה, הערך של [yourdevicevendor] הוא 057e אם המכשיר הוא Nintendo Switch Joy-Con, למשל. אפשר גם להוסיף את ATTRS{idProduct} כדי ליצור כלל ספציפי יותר. ודאו שהחשבון user הוא חבר בקבוצה plugdev. לאחר מכן, מחברים מחדש את המכשיר.

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

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

הדגמות

הדגמות מסוימות של WebHID מפורטות בכתובת web.dev/hid-examples. כדאי לך להיכנס ולראות.

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

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

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

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

חשוב לדעת שמכשירי HID עם רגישות אבטחה (כמו מכשירי HID של FIDO המשמשים לאימות חזק יותר) חסומים גם ב-Chrome. רשימת החסימות של USB ורשימת החסימות של HID

משוב

צוות Chrome ישמח לשמוע את דעתכם על WebHID API ועל הניסיון שלכם איתו.

תיאור של עיצוב ה-API

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

אתם יכולים לשלוח דיווח על בעיה במפרט במאגר GitHub של WebHID API או להוסיף את המחשבות שלכם לבעיה קיימת.

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

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

איך שולחים באגים בנושא WebHID חשוב לכלול כמה שיותר פרטים, לספק הוראות פשוטות לשחזור הבאג ולהגדיר את Components כ-Blink>HID. Glitch הוא כלי מצוין לשיתוף שחזור מהיר וקל של הבעיה.

הצגת תמיכה

אתם מתכננים להשתמש ב-WebHID API? התמיכה הציבורית שלכם עוזרת לצוות Chrome לקבוע את סדר העדיפויות של התכונות, ומראה לספקי דפדפנים אחרים כמה חשובה התמיכה בהן.

שולחים ציוץ אל @ChromiumDev עם ההאשטאג #WebHID ומספרים לנו איפה ואיך אתם משתמשים בו.

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

תודות

תודה לMatt Reynolds ול-Joe Medley על הביקורות שלהם על המאמר הזה. תמונה של Nintendo Switch בצבע אדום וכחול, צילום: Sara Kurfeß. תמונה של מחשב נייד בצבע שחור וכסוף, צילום: Athul Cyriac Ajay ב-Unsplash.