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

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

François Beaufort
François Beaufort

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

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

לפני שנתחיל

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

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

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

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

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

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

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

דרישות אבטחה

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

רק HTTPS

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

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

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

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

כניסה לקוד

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

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

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

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

לתשומת ליבך, אפשר גם להוסיף האזנה לאירוע של 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() ולהסיר באופן תקין את characteristicvaluechanged שנוספו event listener.

התנתקות ממכשיר 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

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

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

בדוגמה הבאה, 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.
  • איפוס האנרגיה - איפוס האנרגיה שנוצלה ממכשיר BLE המשמש לפרסום דופק.
  • מאפיינים – הצגת כל המאפיינים של מאפיין ספציפי ממכשיר BLE.
  • התראות – הפעלה והפסקה של התראות אופייניות ממכשיר BLE.
  • ניתוק מכשיר – ניתוק מכשיר BLE וקבלת התראה על ניתוק מכשיר BLE אחרי ההתחברות אליו.
  • Get Characteristics (קבלת מאפיינים) – גישה לכל המאפיינים של שירות שפורסם ממכשיר BLE.
  • Get descriptors (קבלת תיאורים) - get all properties' (קבלת כל המאפיינים) שמתארים שירות שפורסם ממכשיר BLE.
  • מסנן הנתונים של היצרן – אחזור פרטים בסיסיים של המכשיר ממכשיר BLE שתואם לנתוני היצרן.
  • מסנני החרגה – אחזור פרטים בסיסיים על המכשיר ממכשיר BLE עם מסננים בסיסיים של החרגה.

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

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

ספריות

  • web-bluetooth-utils הוא מודול NPM שמוסיף פונקציות נוחות ממשק ה-API.
  • ספריית shim של Web Bluetooth API זמינה ב-noble, ה-Node.js BLE הפופולרי ביותר מודול מרכזי. כך תוכלו להשתמש ב-webpack/browserify בצורה אצילית בלי שתצטרכו עבור שרת WebSocket או יישומי פלאגין אחרים.
  • angular-web-bluetooth הוא מודול ל-Angular שמפשט את כל נדרש כדי להגדיר את 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 יעקוב אחר שירותי Bluetooth GATT חדשים שהתגלו ואילו אירוע serviceremoved יעקוב אחר אירועים שהוסרו. servicechanged חדש האירוע יופעל מיד כשיתווספו מאפיין ו/או מתאר כלשהו, או הוסר משירות Bluetooth GATT.

הצגת תמיכה ב-API

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

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

משאבים

אישורים

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