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

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

François Beaufort
François Beaufort

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

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

לפני שנתחיל

ההנחה במסמך הזה היא שיש לכם ידע בסיסי באופן שבו פועלים Bluetooth Low Energy‏ (BLE) וGeneric Attribute Profile.

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

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

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

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

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

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

דרישות אבטחה

כדי להבין את הפשרות בנושא אבטחה, מומלץ לקרוא את המאמר Web Bluetooth Security Model (מודל האבטחה של Web Bluetooth) של Jeffrey Yasskin, מהנדס תוכנה בצוות Chrome שעובד על מפרט Web Bluetooth API.

רק HTTPS

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

נדרשת תנועה של המשתמש

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

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

איך נכנסים לקוד

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

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

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

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

הנחיה למשתמש לגבי מכשיר Bluetooth.

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

מסנן שירותים

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

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

עם זאת, אם שירות ה-GATT של Bluetooth לא מופיע ברשימת שירותי ה-GATT של Bluetooth שהוגדרו כסטנדרט, אפשר לספק את ה-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); });

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

שימו לב שאפשר גם להוסיף מאזין לאירועים מסוג 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

כתיבת למאפיין Bluetooth GATT היא פשוט כמו קריאה שלו. הפעם נשתמש בנקודת הבקרה על הדופק כדי לאפס את הערך של השדה 'צריכת אנרגיה' ל-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

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

רמה למתחילים

  • פרטי המכשיר – אחזור פרטי מכשיר בסיסיים ממכשיר BLE.
  • רמת הסוללה - אחזור פרטי הסוללה ממידע על הסוללה לפרסום במכשיר BLE.
  • Reset Energy – איפוס האנרגיה שהוצאה ממכשיר BLE שמפרסם את הדופק.
  • מאפיינים – הצגת כל המאפיינים של מאפיין ספציפי ממכשיר BLE.
  • התראות – הפעלה והפסקה של התראות על מאפיינים ממכשיר BLE.
  • ניתוק המכשיר – ניתוק של מכשיר BLE לאחר החיבור אליו, וקבלת התראה על הניתוק.
  • Get Characteristics – אחזור כל המאפיינים של שירות שפורסם ממכשיר BLE.
  • Get Descriptors – אחזור כל מתארי המאפיינים של שירות שפורסם ממכשיר BLE.
  • מסנן הנתונים של היצרן – אחזור פרטים בסיסיים של המכשיר ממכשיר BLE שתואם לנתוני היצרן.
  • מסנני החרגה – אחזור פרטים בסיסיים על המכשיר ממכשיר BLE עם מסננים בסיסיים של החרגה.

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

מומלץ גם לעיין בהדגמות נבחרות של Web Bluetooth ובCodelabs הרשמיים של Web Bluetooth.

ספריות

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

כלים

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

טיפים

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

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

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

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

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

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

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

תמיכה ב-API

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

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

משאבים

תודות

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