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

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

François Beaufort
François Beaufort

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

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

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

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

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

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

מכשיר HID (ממשק אנושי) הוא סוג של מכשיר שמקבל קלט מבני אדם או מספק פלט לבני אדם. המונח מתייחס גם לפרוטוקול 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]}.`);
});

אפשר לראות את ההדגמה של webhid-joycon-button.

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

כדי לשלוח דוח פלט למכשיר 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));

אפשר לראות את ההדגמה של webhid-joycon-rumble ב-Pen.

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

דוחות התכונות הם הסוג היחיד של דוחות נתוני 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);
}

אפשר לעיין בהדגמה של webhid-apple-keyboard-backlight.

כדי לקבל דוח תכונות ממכשיר 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 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 שרגישים לאבטחה (כמו מכשירי FIDO HID שמשמשים לאימות חזק יותר) נחסמים גם ב-Chrome. מעיינים בקבצים USB blocklist ו-HID blocklist.

משוב

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

נשמח לקבל מידע על עיצוב ה-API

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

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

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

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

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

תמיכה ביוצרים

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

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

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

תודות

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