תקשורת עם מכשירי Bluetooth באמצעות JavaScript

ה-Web Bluetooth API מאפשר לאתרים לתקשר עם מכשירי Bluetooth.

François Beaufort
François Beaufort

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

עד עכשיו, היכולת לקיים אינטראקציה עם מכשירי Bluetooth הייתה אפשרית רק לאפליקציות ספציפיות לפלטפורמה. המטרה של ה-Web Bluetooth API היא לשנות זאת ולהביא אותו גם לדפדפני אינטרנט.

לפני שנתחיל

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

על אף שמפרט Web Bluetooth API עדיין לא סופי, מחברי המפרט מחפשים באופן פעיל מפתחים נלהבים לנסות את ה-API הזה ולתת משוב על המפרט ומשוב על ההטמעה.

קבוצת משנה של ה-Web Bluetooth API זמינה ב-ChromeOS, ב-Chrome ל-Android 6.0, ב-Mac (Chrome 56) וב-Windows 10 (Chrome 70). פירוש הדבר הוא שתהיה לך אפשרות לבקש ולהתחבר למכשירים בקרבת מקום עם צריכת אנרגיה נמוכה, לקרוא/לכתוב מאפייני Bluetooth, לקבל התראות GATT, לדעת כשמכשיר Bluetooth מתנתק, ואפילו לקרוא ולכתוב לתיאורי Bluetooth. למידע נוסף, ראו את הטבלה תאימות דפדפן של MDN.

ב-Linux ובגרסאות קודמות של Windows, מפעילים את הדגל #experimental-web-platform-features ב-about://flags.

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

כדי לקבל כמה שיותר משוב ממפתחים שמשתמשים ב-Web Bluetooth API בשדה, Chrome הוסיף בעבר את התכונה הזו ב-Chrome 53 כגרסת מקור לניסיון ל-ChromeOS, ל-Android ול-Mac.

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

דרישות אבטחה

כדי להבין את הטרנזקציות באבטחה, מומלץ לקרוא את הפוסט בנושא Web Bluetooth SecurityModel מאת ג'פרי יאסקין, מהנדס תוכנה בצוות Chrome, שעונה על המפרט של Web Bluetooth API.

רק HTTPS

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

יש לבצע פעולת משתמש

כאמצעי אבטחה, הגילוי של מכשירי Bluetooth באמצעות navigator.bluetooth.requestDevice חייב להתבצע באמצעות תנועת משתמש כמו מגע או לחיצה על העכבר. מומלץ להאזין לאירועים של pointerup, click ושל touchend.

button.addEventListener('pointerup', function(event) {
  // Call navigator.bluetooth.requestDevice
});

כניסה לקוד

ה-Web Bluetooth API מסתמך במידה רבה על הבטחות של JavaScript. אם אתם לא מכירים אותן, מומלץ לעיין במדריך הנהדר הזה בנושא הבטחות. דבר נוסף הוא () => {}: פונקציות החץ של ECMAScript 2015.

בקשה למכשירי Bluetooth

הגרסה הזו של מפרט ה-Web Bluetooth API מאפשרת לאתרים, שפועלים בתפקיד Central, להתחבר לשרתי GATT מרוחקים באמצעות חיבור BLE. הוא תומך בתקשורת בין מכשירים עם Bluetooth מגרסה 4.0 ואילך.

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

הודעה למשתמש במכשיר Bluetooth.

הפונקציה navigator.bluetooth.requestDevice() מקבלת אובייקט חובה שמגדיר מסננים. המסננים האלה משמשים להחזרת רק מכשירים שתואמים לשם של שירותי Bluetooth GATT מסוימים או לשם המכשיר.

מסנן השירותים

לדוגמה, כדי לבקש מכשירי Bluetooth שמפרסמים את שירות Bluetooth GATT:

navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => { /* … */ })
.catch(error => { console.error(error); });

עם זאת, אם שירות ה-Bluetooth GATT לא מופיע ברשימה של שירותי ה-Bluetooth GATT התקניים, אפשר לספק את ה-UUID המלא של ה-Bluetooth או טופס קצר של 16 או 32 ביט.

navigator.bluetooth.requestDevice({
  filters: [{
    services: [0x1234, 0x12345678, '99999999-0000-1000-8000-00805f9b34fb']
  }]
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

מסנן שמות

אפשר גם לבקש מכשירי Bluetooth על סמך שם המכשיר שפורסם באמצעות מפתח המסננים name, או אפילו קידומת של השם הזה עם מפתח המסננים namePrefix. שימו לב שבמקרה הזה צריך גם להגדיר את המפתח optionalServices כדי לגשת לשירותים שלא נכללים במסנן השירות. אם לא תעשו זאת, תופיע הודעת שגיאה מאוחר יותר כשתנסו לגשת אליהם.

navigator.bluetooth.requestDevice({
  filters: [{
    name: 'Francois robot'
  }],
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

מסנן נתוני היצרן

אפשר גם לבקש מכשירי Bluetooth על סמך נתונים ספציפיים ליצרן שמתפרסמים באמצעות מפתח המסננים manufacturerData. המפתח הזה הוא מערך של אובייקטים עם מפתח מזהה של חברת Bluetooth שנקרא companyIdentifier. ניתן לך גם לציין קידומת נתונים שמסננת את נתוני היצרן ממכשירי Bluetooth שמתחילים בה. שימו לב שתצטרכו גם להגדיר את המפתח optionalServices כדי לקבל גישה לשירותים שלא כלולים במסנן שירות. אם לא תעשו זאת, תופיע הודעת שגיאה מאוחר יותר כשתנסו לגשת אליהם.

// Filter Bluetooth devices from Google company with manufacturer data bytes
// that start with [0x01, 0x02].
navigator.bluetooth.requestDevice({
  filters: [{
    manufacturerData: [{
      companyIdentifier: 0x00e0,
      dataPrefix: new Uint8Array([0x01, 0x02])
    }]
  }],
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

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

מסנני החרגה

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

// Request access to a bluetooth device whose name starts with "Created by".
// The device named "Created by Francois" has been reported as unsupported.
navigator.bluetooth.requestDevice({
  filters: [{
    namePrefix: "Created by"
  }],
  exclusionFilters: [{
    name: "Created by Francois"
  }],
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

בלי פילטרים

לבסוף, במקום filters, אפשר להשתמש במפתח acceptAllDevices כדי להציג את כל מכשירי ה-Bluetooth בקרבת מקום. צריך גם להגדיר את המפתח optionalServices כדי לקבל גישה לשירותים מסוימים. אם לא תעשו זאת, תוצג הודעת שגיאה מאוחר יותר כשתנסו לגשת אליהם.

navigator.bluetooth.requestDevice({
  acceptAllDevices: true,
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

התחברות למכשיר Bluetooth

מה את/ה עושה עכשיו, אחרי שיש לך BluetoothDevice? בואו מתחברים לשרת GATT המרוחק של Bluetooth, שמכיל את השירות ואת הגדרות המאפיינים.

navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => {
  // Human-readable name of the device.
  console.log(device.name);

  // Attempts to connect to remote GATT Server.
  return device.gatt.connect();
})
.then(server => { /* … */ })
.catch(error => { console.error(error); });

הקראת מאפיין Bluetooth

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

בדוגמה הבאה, battery_level הוא המאפיין הסטנדרטי של רמת הסוללה.

navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => device.gatt.connect())
.then(server => {
  // Getting Battery Service…
  return server.getPrimaryService('battery_service');
})
.then(service => {
  // Getting Battery Level Characteristic…
  return service.getCharacteristic('battery_level');
})
.then(characteristic => {
  // Reading Battery Level…
  return characteristic.readValue();
})
.then(value => {
  console.log(`Battery percentage is ${value.getUint8(0)}`);
})
.catch(error => { console.error(error); });

אם אתם משתמשים במאפיין GATT בהתאמה אישית של Bluetooth, תוכלו לספק ל-service.getCharacteristic את ה-UUID המלא של ה-Bluetooth או טופס קצר של 16 או 32 ביט.

שימו לב שאפשר גם להוסיף מאפיין event listener של characteristicvaluechanged למאפיין כדי לקרוא את הערך שלו. כדאי לקרוא את הדגלמת קריאה של ערך שינוי ערך כדי לראות איך לטפל באופן אופציונלי גם בהתראות GATT הבאות.

…
.then(characteristic => {
  // Set up event listener for when characteristic value changes.
  characteristic.addEventListener('characteristicvaluechanged',
                                  handleBatteryLevelChanged);
  // Reading Battery Level…
  return characteristic.readValue();
})
.catch(error => { console.error(error); });

function handleBatteryLevelChanged(event) {
  const batteryLevel = event.target.value.getUint8(0);
  console.log('Battery percentage is ' + batteryLevel);
}

כתיבה למאפיין Bluetooth

הכתיבה למאפיין GATT של Bluetooth קלה ממש כמו לקרוא אותו. הפעם נשתמש בנקודת בקרת הדופק כדי לאפס את הערך בשדה Energy Expended 0 במכשיר מוניטור דופק.

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

navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('heart_rate'))
.then(service => service.getCharacteristic('heart_rate_control_point'))
.then(characteristic => {
  // Writing 1 is the signal to reset energy expended.
  const resetEnergyExpended = Uint8Array.of(1);
  return characteristic.writeValue(resetEnergyExpended);
})
.then(_ => {
  console.log('Energy expended has been reset.');
})
.catch(error => { console.error(error); });

קבלת התראות GATT

עכשיו נראה איך לקבל התראה כשהמאפיין מדידת קצב הלב משתנה במכשיר:

navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('heart_rate'))
.then(service => service.getCharacteristic('heart_rate_measurement'))
.then(characteristic => characteristic.startNotifications())
.then(characteristic => {
  characteristic.addEventListener('characteristicvaluechanged',
                                  handleCharacteristicValueChanged);
  console.log('Notifications have been started.');
})
.catch(error => { console.error(error); });

function handleCharacteristicValueChanged(event) {
  const value = event.target.value;
  console.log('Received ' + value);
  // TODO: Parse Heart Rate Measurement value.
  // See https://github.com/WebBluetoothCG/demos/blob/gh-pages/heart-rate-sensor/heartRateSensor.js
}

בדוגמה להודעות מוסבר איך להפסיק את ההתראות באמצעות stopNotifications() ולהסיר באופן תקין את ה-event listener של characteristicvaluechanged.

התנתקות ממכשיר Bluetooth

כדי לספק חוויית משתמש טובה יותר, כדאי להאזין לאירועי ניתוק ולהזמין את המשתמש להתחבר מחדש:

navigator.bluetooth.requestDevice({ filters: [{ name: 'Francois robot' }] })
.then(device => {
  // Set up event listener for when device gets disconnected.
  device.addEventListener('gattserverdisconnected', onDisconnected);

  // Attempts to connect to remote GATT Server.
  return device.gatt.connect();
})
.then(server => { /* … */ })
.catch(error => { console.error(error); });

function onDisconnected(event) {
  const device = event.target;
  console.log(`Device ${device.name} is disconnected.`);
}

אפשר גם להתקשר אל device.gatt.disconnect() כדי לנתק את אפליקציית האינטרנט ממכשיר ה-Bluetooth. הפעולה הזו תפעיל מאזינים קיימים של אירועים מסוג gattserverdisconnected. שים לב: הפעולה הזו לא תפסיק את התקשורת עם מכשיר ה-Bluetooth אם אפליקציה אחרת כבר מתקשרת עם מכשיר ה-Bluetooth. כדי להתעמק יותר בנושא, תוכלו להיעזר בדגימת ניתוק מכשירים ובדגימת חיבור מחדש אוטומטי.

קריאה וכתיבה למתארי Bluetooth

תיאורי GATT של Bluetooth הם מאפיינים שמתארים ערך מאפיין. אפשר לקרוא ולכתוב אותם באופן דומה למאפייני GATT של Bluetooth.

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

בדוגמה הבאה, health_thermometer הוא שירות מדחום בריאות, measurement_intervalמאפיין מרווח המדידה וgatt.characteristic_user_description תיאור תיאור המשתמש.

navigator.bluetooth.requestDevice({ filters: [{ services: ['health_thermometer'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('health_thermometer'))
.then(service => service.getCharacteristic('measurement_interval'))
.then(characteristic => characteristic.getDescriptor('gatt.characteristic_user_description'))
.then(descriptor => descriptor.readValue())
.then(value => {
  const decoder = new TextDecoder('utf-8');
  console.log(`User Description: ${decoder.decode(value)}`);
})
.catch(error => { console.error(error); });

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

navigator.bluetooth.requestDevice({ filters: [{ services: ['health_thermometer'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('health_thermometer'))
.then(service => service.getCharacteristic('measurement_interval'))
.then(characteristic => characteristic.getDescriptor('gatt.characteristic_user_description'))
.then(descriptor => {
  const encoder = new TextEncoder('utf-8');
  const userDescription = encoder.encode('Defines the time between measurements.');
  return descriptor.writeValue(userDescription);
})
.catch(error => { console.error(error); });

טעימות, הדגמות ו-Codelabs

כל דוגמאות ה-Bluetooth לאינטרנט נבדקו בהצלחה. כדי להפיק את המרב מהדגימות האלה, מומלץ להתקין את [סימולטור הציוד ההיקפי BLE אפליקציית Android] שמדמה ציוד היקפי של BLE באמצעות סוללה, שירות דופק או שירות של מדחום בריאות.

מתחילים

  • מידע על המכשיר - אחזור מידע בסיסי של המכשיר ממכשיר BLE.
  • רמת הסוללה – אחזור פרטי הסוללה מ-BLE Device שמפרסמת נתוני סוללה.
  • איפוס אנרגיה – איפוס האנרגיה שנצרכת מדופק שמנוצל על ידי מכשיר BLE.
  • תכונות מאפיינים – הצגת כל המאפיינים של מאפיין ספציפי ממכשיר BLE.
  • התראות – הפעלה ועצירה של התראות בעלות מאפיינים ממכשיר BLE.
  • ניתוק מכשיר - ניתוק מכשיר BLE וקבלת הודעה ממנו לאחר ההתחברות אליו.
  • קבלת מאפיינים – עליכם לקבל את כל המאפיינים של השירות שפורסם במכשיר BLE.
  • קבלת תיאורים – צריך להשתמש במכשיר BLE שמתאר את כל המאפיינים של שירות שפורסם.
  • מסנן נתונים של היצרן – אחזור מידע בסיסי על המכשיר ממכשיר BLE שתואם לנתוני היצרן.
  • מסנני החרגה - אחזור מידע בסיסי על המכשיר ממכשיר BLE הכולל מסנני החרגה בסיסיים.

שילוב של פעולות מרובות

כדאי גם לעיין באוסף הדגמות ה-Bluetooth באינטרנט וב-Codelabs הרשמיים לאינטרנט.

ספריות

  • web-bluetooth-utils הוא מודול npm שמוסיף כמה פונקציות נוחות ל-API.
  • ספריית shim של Web Bluetooth API זמינה ב-noble, המודול המרכזי הפופולרי ביותר מסוג Node.js BLE. כך תוכלו לפרוס את webpack/browserify ב-noble בלי צורך בשרת WebSocket או ביישומי פלאגין אחרים.
  • angular-web-Bluetooth הוא מודול של Angular שמפשט את כל תבנית ה-boilerplate שנדרשת כדי להגדיר את Web Bluetooth API.

כלים

  • תחילת העבודה עם Bluetooth באינטרנט היא אפליקציית אינטרנט פשוטה שמפיקה את כל הקוד הסטנדרטי של JavaScript כדי להתחיל אינטראקציה עם מכשיר Bluetooth. הזינו שם של מכשיר, שירות ומאפיין, מגדירים את המאפיינים שלו, וזהו זה.
  • אם אתם כבר מפתחי Bluetooth, הפלאגין של Web Bluetooth Developer Studio ייצור גם את קוד ה-JavaScript של ה-Bluetooth באינטרנט בשביל מכשיר ה-Bluetooth.

טיפים

דף Bluetooth Internal זמין ב-Chrome בכתובת about://bluetooth-internals, כדי שתוכלו לבדוק את כל מה שקשור למכשירי Bluetooth בקרבת מקום: סטטוס, שירותים, מאפיינים ומתארים.

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

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

המאמרים הבאים

קודם צריך לבדוק את סטטוס ההטמעה של הדפדפן והפלטפורמה כדי לדעת אילו חלקים של Web Bluetooth API מוטמעים כרגע.

על אף שהיא עדיין חלקית, הנה הצצה למה לצפות בעתיד הקרוב:

  • סריקה לאיתור מודעות BLE בקרבת מקום תתבצע באמצעות navigator.bluetooth.requestLEScan().
  • אירוע חדש של serviceadded יעקוב אחר שירותי GATT של Bluetooth שנמצאו לאחרונה ואירוע serviceremoved יעקוב אחר שירותי GATT שהוסרו. אירוע servicechanged חדש יופעל כשיתווספו או יוסרו מאפיין או מתאר כלשהו משירות GATT של Bluetooth.

הבעת תמיכה ב-API

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

שלח ציוץ אל @ChromiumDev באמצעות ה-hashtag #WebBluetooth וספר לנו איפה אתם משתמשים בו ואיך אתם משתמשים בו.

משאבים

אישורים

תודה ל-Kayce Basques על הביקורת של מאמר זה. תמונה ראשית (Hero) מאת SparkFun Electronics מבולדר, ארה"ב.