Komunikacja z urządzeniami Bluetooth przez JavaScript

Interfejs Web Bluetooth API pozwala witrynom komunikować się z urządzeniami Bluetooth.

François Beaufort
François Beaufort

A jeśli powiem Ci, że strony mogą komunikować się z urządzeniami Bluetooth w pobliżu w sposób bezpieczny i chroniący prywatność? Dzięki temu czujniki tętna, śpiewające żarówki, a nawet żółwie, będą mogły wchodzić w bezpośrednią interakcję ze stroną internetową.

Do tej pory interakcja z urządzeniami Bluetooth była możliwa tylko w przypadku aplikacji na konkretnej platformie. Interfejs Web Bluetooth API ma to zmienić i udostępniać również w przeglądarkach.

Zanim zaczniemy

Zakładamy w nim, że masz podstawową wiedzę na temat działania Bluetooth Low Energy (BLE) i jak działają ogólny profil atrybutu.

Mimo że specyfikacja interfejsu Web Bluetooth API nie jest jeszcze gotowa, jej autorzy aktywnie szukają entuzjastycznych programistów, którzy mogliby wypróbować ten interfejs API i wyrazić opinię o specyfikacji oraz opinie na temat jego implementacji.

Część funkcji Web Bluetooth API jest dostępna w systemach ChromeOS, Chrome na Androida 6.0, Mac (Chrome 56) i Windows 10 (Chrome 70). Oznacza to, że masz możliwość wysyłania próśb o urządzenia Bluetooth Low Energy i łączenia się z nimi, odczytu/zapisu charakterystyki Bluetooth, odbierania powiadomień GATT, otrzymywania informacji o odłączeniu urządzenia Bluetooth, a nawet odczytu i zapisu w deskryptorach Bluetooth. Więcej informacji znajdziesz w tabeli MDN na temat zgodności przeglądarek.

W przypadku Linuksa i starszych wersji systemu Windows włącz flagę #experimental-web-platform-features w about://flags.

Dostępne w przypadku testowania origin

Aby uzyskać jak największą liczbę opinii od deweloperów używających w tym polu interfejsu Web Bluetooth API, Chrome dodał wcześniej tę funkcję do Chrome 53 w ramach wersji próbnej dla ChromeOS, Androida i Maca.

Okres próbny zakończył się w styczniu 2017 roku.

Wymagania dotyczące bezpieczeństwa

Aby zrozumieć straty w zakresie bezpieczeństwa, polecam post Web Bluetooth Security Model autorstwa Jeffreya Yasskina, inżyniera oprogramowania z zespołu Chrome, który pracuje nad specyfikacją interfejsu Web Bluetooth API.

Tylko HTTPS

Ten eksperymentalny interfejs API to nowa, zaawansowana funkcja dodana do internetu, dlatego jest dostępna tylko w bezpiecznych kontekstach. Oznacza to, że musisz pamiętać o TLS.

Wymagany gest użytkownika

W ramach funkcji zabezpieczeń wykrywanie urządzeń Bluetooth z navigator.bluetooth.requestDevice musi być aktywowane przez gest użytkownika, np. dotknięcie lub kliknięcie myszą. Mówimy o słuchaniu zdarzeń pointerup, click i touchend.

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

Wejdź do kodu

Interfejs Web Bluetooth API w dużym stopniu opiera się na obietnicach JavaScriptu. Jeśli nie znasz tych funkcji, zapoznaj się z tym świetnym samouczkiem dotyczącym obietnic. I jeszcze jedno: () => {} to funkcje strzałek zgodne z ECMAScript 2015.

Żądanie urządzeń Bluetooth

Ta wersja specyfikacji interfejsu Web Bluetooth API umożliwia witrynom działającym w roli Centralnej łączenie się ze zdalnymi serwerami GATT przez połączenie BLE. Obsługuje komunikację między urządzeniami z zastosowaniem Bluetootha 4.0 lub nowszym.

Gdy strona prosi o dostęp do urządzeń w pobliżu za pomocą navigator.bluetooth.requestDevice, przeglądarka wyświetla komunikat z selektorem urządzenia, za pomocą którego może wybrać jedno urządzenie lub anulować prośbę.

Prompt użytkownika urządzenia Bluetooth.

Funkcja navigator.bluetooth.requestDevice() przyjmuje obowiązkowy obiekt, który określa filtry. Te filtry służą do zwracania tylko urządzeń, które pasują do niektórych reklamowanych usług GATT Bluetooth lub nazwy urządzenia.

Filtr usług

Aby np. wysłać żądanie urządzeń Bluetooth reklamujących usługę baterii Bluetooth GATT:

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

Jeśli usługi Bluetooth GATT nie ma na liście standardowych usług Bluetooth GATT, możesz podać pełny identyfikator UUID Bluetooth lub krótki, 16- lub 32-bitowy format.

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

Filtr nazw

Za pomocą klucza filtrów name możesz też wysyłać żądania urządzeń Bluetooth na podstawie ich rozgłaszanej nazwy, a nawet prefiksu tej nazwy – kluczem filtrów namePrefix. Pamiętaj, że w tym przypadku musisz też zdefiniować klucz optionalServices, aby uzyskać dostęp do usług, które nie są uwzględnione w filtrze usług. Jeśli tego nie zrobisz, podczas próby uzyskania dostępu pojawi się błąd.

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

Filtr danych producenta

Możesz też wysyłać żądania urządzeń Bluetooth na podstawie danych producenta reklamowanych za pomocą klucza filtra manufacturerData. Ten klucz to tablica obiektów z obowiązkowym kluczem identyfikatora firmy Bluetooth o nazwie companyIdentifier. Możesz też podać prefiks danych, który filtruje dane producenta z urządzeń Bluetooth, które zaczynają się od takiego urządzenia. Pamiętaj, że musisz też zdefiniować klucz optionalServices, aby uzyskać dostęp do usług, których nie obejmuje filtr usług. Jeśli tego nie zrobisz, później podczas próby uzyskania dostępu do tych plików pojawi się błąd.

// 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); });

Maski można też używać z prefiksem danych, aby dopasować niektóre wzorce w danych producenta. Aby dowiedzieć się więcej, sprawdź wyjaśnienie dotyczące filtrów danych Bluetooth.

Filtry wykluczania

Opcja exclusionFilters w interfejsie navigator.bluetooth.requestDevice() pozwala wykluczyć niektóre urządzenia z selektora przeglądarki. Można go używać do wykluczania urządzeń, które pasują do szerszego filtra, ale nie są obsługiwane.

// 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); });

Brak filtrów

Zamiast filters możesz też użyć klawisza acceptAllDevices, by wyświetlić wszystkie urządzenia Bluetooth znajdujące się w pobliżu. Aby uzyskać dostęp do niektórych usług, musisz też zdefiniować klucz optionalServices. Jeśli tego nie zrobisz, później podczas próby uzyskania dostępu wystąpi błąd.

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

Nawiązywanie połączenia z urządzeniem Bluetooth

Co zrobić, gdy masz już BluetoothDevice? Połączmy się ze zdalnym serwerem GATT, który posiada usługę i definicje właściwości.

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); });

Odczytywanie cechy Bluetooth

Łączymy się z serwerem GATT zdalnego urządzenia Bluetooth. Teraz chcemy uzyskać podstawową usługę GATT i odczytać jej cechy. Spróbujmy na przykład sprawdzić bieżący poziom naładowania baterii urządzenia.

W poniższym przykładzie battery_level to standardowa cecha poziomu baterii.

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); });

Jeśli używasz niestandardowej cechy GATT Bluetooth, możesz podać service.getCharacteristic pełny identyfikator UUID Bluetootha albo krótki, 16- lub 32-bitowy format.

Pamiętaj, że możesz też dodać do danej cechy odbiornik zdarzenia characteristicvaluechanged, który będzie obsługiwać odczyt jej wartości. Zapoznaj się z przykładem odczytu danych o zmianie wartości cechy, aby dowiedzieć się, jak opcjonalnie postępować z nadchodzącymi powiadomieniami 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);
}

Zapisz w charakterze Bluetooth

Zapisywanie cech GATT jest tak proste, jak ich odczytanie. Tym razem skorzystamy z punktu kontroli tętna, aby zresetować wartość w polu „Wyczerpane kalorie” na 0 na pulsierze.

Zapewniam, że nie ma tu magii. Wszystko wyjaśniono na stronie z cechami punktu kontrolnego tętna.

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); });

Otrzymuj powiadomienia GATT

A teraz zobaczmy, jak otrzymywać powiadomienia, gdy na urządzeniu zmieni się charakterystyka pomiaru tętna:

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
}

Z przykładowego powiadomienia dowiesz się, jak wyłączyć powiadomienia za pomocą stopNotifications() i prawidłowo usunąć dodany detektor zdarzeń characteristicvaluechanged.

Rozłączanie z urządzeniem Bluetooth

Aby zapewnić użytkownikom lepsze wrażenia, możesz wyłapać zdarzenia rozłączenia i zaprosić ich do ponownego połączenia:

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.`);
}

Możesz też zadzwonić pod numer device.gatt.disconnect(), aby odłączyć aplikację internetową od urządzenia Bluetooth. Spowoduje to aktywowanie istniejących detektorów zdarzeń gattserverdisconnected. Pamiętaj, że to NIE zatrzyma komunikacji przez urządzenie Bluetooth, jeśli inna aplikacja komunikuje się już z urządzeniem Bluetooth. Aby dowiedzieć się więcej, zapoznaj się z materiałami przykładowymi odłączenia urządzenia i przykładem automatycznego ponownego łączenia.

Odczyt i zapis w deskryptorach Bluetooth

Deskryptory Bluetooth GATT to atrybuty opisujące wartość charakterystyczną. Możesz je odczytywać i zapisywać w sposób podobny do funkcji Bluetooth GATT.

Zobaczmy na przykład, jak odczytywać opis użytkownika, który wskazuje, jaka wartość wskazuje, ile wynosi interwał pomiarowy termometru stanu urządzenia.

W poniższym przykładzie health_thermometer to usługa Termometru zdrowotnego, measurement_interval interwał pomiaru i gatt.characteristic_user_description deskryptor opisu charakteru użytkownika.

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); });

Skoro znamy już opis podany przez użytkownika przedziału czasowego pomiaru termometru stanu urządzenia, zobaczmy, jak go zaktualizować i zapisać wartość niestandardową.

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); });

Przykłady, wersje demonstracyjne i ćwiczenia z programowania

Wszystkie poniższe przykłady Web Bluetooth zostały przetestowane. Aby w pełni korzystać z tych próbek, zainstaluj aplikację [BLE Peryheral Simulator na Androida], która symuluje urządzenie peryferyjne BLE przy użyciu usługi baterii, usługi pomiaru tętna lub Health Thermometer.

Początkujący

  • Informacje o urządzeniu – pobieranie podstawowych informacji o urządzeniu z urządzenia z BLE.
  • Poziom baterii – informacje o baterii pochodzące z informacji o baterii wyświetlanych na urządzeniu z BLE.
  • Zresetuj energię – zresetuj energię wydaną przez urządzenie BLE wyświetlające tętno.
  • Właściwości charakterystyczne – wyświetlanie wszystkich właściwości określonych cech z urządzenia z BLE.
  • Powiadomienia – włączaj i wyłączaj powiadomienia charakterystyczne z urządzenia BLE.
  • Rozłączanie urządzenia – rozłącz się i otrzymaj powiadomienie o odłączeniu urządzenia BLE po połączeniu się z nim.
  • Poznaj cechy – uzyskaj wszystkie cechy reklamowanej usługi z urządzenia z BLE.
  • Pobierz deskryptory – pobierz deskryptory wszystkich cech reklamowanej usługi z urządzenia z BLE.
  • Filtr danych producenta – pobiera podstawowe informacje z urządzenia z BLE, które są zgodne z danymi producenta.
  • Filtry wykluczania – umożliwia pobieranie podstawowych informacji o urządzeniu z urządzenia BLE z podstawowymi filtrami wykluczania.

Łączenie wielu operacji

Zapoznaj się też z naszymi specjalnymi prezentacjami Web Bluetooth i oficjalnymi laboratoriami Web Bluetooth Codelabs.

Biblioteki

  • web-bluetooth-utils to moduł npm, który dodaje do interfejsu API pewne przydatne funkcje.
  • Podkładka Web Bluetooth API jest dostępna w noble, najpopularniejszym module głównym Node.js BLE. Dzięki temu możesz tworzyć pakiety internetowe dla przeglądarki Noble bez konieczności używania serwera WebSocket ani innych wtyczek.
  • Angular-web-Bluetooth to moduł dla języka Angular, który wyodrębnia wszystkie elementy niezbędne do skonfigurowania interfejsu Web Bluetooth API.

Narzędzia,

  • Wypróbuj Web Bluetooth to prosta aplikacja internetowa, która generuje cały stały kod JavaScript, aby rozpocząć interakcję z urządzeniem Bluetooth. Wpisz nazwę urządzenia, usługę lub cechę, określ jego właściwości i gotowe.
  • Jeśli jesteś już programistą Bluetooth, wtyczka Web Bluetooth Developer Studio wygeneruje też kod JavaScript Web Bluetooth dla Twojego urządzenia Bluetooth.

Wskazówki

W Chrome na stronie about://bluetooth-internals dostępna jest strona wewnętrznego modułu Bluetooth, na której możesz sprawdzić wszystkie informacje o urządzeniach Bluetooth znajdujących się w pobliżu: stan, usługi, cechy i deskryptory.

Zrzut ekranu przedstawiający stronę wewnętrzną do debugowania Bluetooth w Chrome
Strona wewnętrzna w Chrome na potrzeby debugowania urządzeń Bluetooth.

Zalecamy też zapoznanie się z oficjalną stroną Zgłaszanie błędów Web Bluetooth, ponieważ debugowanie Bluetootha może czasem być trudne.

Co dalej

Najpierw sprawdź stan implementacji przeglądarki i platformy, aby dowiedzieć się, które części interfejsu Web Bluetooth API są obecnie zaimplementowane.

Ten dokument nie jest jeszcze w pełni zakończony, ale poniżej zamieszczamy zapowiedź tego, czego można się spodziewać w najbliższej przyszłości:

  • Skanowanie w poszukiwaniu reklam BLE w pobliżu odbywa się w ramach usługi navigator.bluetooth.requestLEScan().
  • Nowe zdarzenie serviceadded będzie śledzić nowo wykryte usługi GATT Bluetooth, natomiast zdarzenie serviceremoved śledzi usunięte usługi. Nowe zdarzenie servicechanged zostanie uruchomione, gdy dowolna cecha lub deskryptor zostanie dodana do usługi Bluetooth GATT lub z niej usunięta.

Pokaż obsługę interfejsu API

Czy zamierzasz użyć interfejsu Web Bluetooth API? Twoje publiczne wsparcie pomaga zespołowi Chrome priorytetowo traktować funkcje i pokazuje innym dostawcom przeglądarek, jak ważne jest ich wsparcie.

Wyślij tweeta na adres @ChromiumDev, używając hashtagu #WebBluetooth, i daj nam znać, gdzie i w jaki sposób go używasz.

Zasoby

Podziękowania

Dziękujemy Kayce Basques za przeczytanie tego artykułu. Baner powitalny od SparkFun Electronics z Boulder w Stanach Zjednoczonych.