자바스크립트를 통해 블루투스 기기와 통신하기

Web Bluetooth API를 사용하면 웹사이트에서 블루투스 기기와 통신할 수 있습니다.

François Beaufort
François Beaufort

웹사이트가 개인 정보를 안전하게 보호하는 방식으로 근처 블루투스 기기와 통신할 수 있다면 어떻게 해야 할까요? 이렇게 하면 심박수 모니터, 노래하는 전구, 거북이까지 웹사이트와 직접 상호작용할 수 있습니다.

지금까지는 블루투스 기기와 상호작용하는 기능이 플랫폼별 앱에서만 가능했습니다. Web Bluetooth API는 이를 변경하여 웹브라우저에도 제공하는 것을 목표로 합니다.

시작하기 전에

이 문서에서는 저전력 블루투스 (BLE) 및 일반 속성 프로필의 작동 방식에 관한 기본 지식이 있다고 가정합니다.

Web Bluetooth API 사양이 아직 확정되지는 않았지만 사양 작성자는 이 API를 사용해 보고 사양에 관한 의견구현에 관한 의견을 제공할 열정적인 개발자를 적극적으로 찾고 있습니다.

Web Bluetooth API의 일부는 ChromeOS, Android용 Chrome, Mac (Chrome 56), Windows 10 (Chrome 70)에서 사용할 수 있습니다. 즉, 근처 저전력 블루투스 기기에 요청연결하고, 블루투스 특성을 읽고/쓰기하며, GATT 알림을 수신하고, 블루투스 기기가 연결 해제되는 시점을 인지하고, 블루투스 설명어를 읽고 쓸 수 있어야 합니다. 자세한 내용은 MDN의 브라우저 호환성 표를 참고하세요.

Linux 및 이전 버전의 Windows에서는 about://flags에서 #experimental-web-platform-features 플래그를 사용 설정합니다.

오리진 트라이얼 사용 가능

현장에서 Web Bluetooth API를 사용하는 개발자로부터 최대한 많은 의견을 얻기 위해 Chrome은 이전에 Chrome 53에 이 기능을 ChromeOS, Android, Mac용 오리진 트라이얼로 추가했습니다.

무료 체험이 2017년 1월에 종료되었습니다.

보안 요구사항

보안의 장단점을 알아보려면 Chrome팀의 소프트웨어 엔지니어인 제프리 야스킨이 웹 블루투스 API 사양을 작업하고 있는 웹 블루투스 보안 모델 게시물을 참고하세요.

HTTPS 전용

이 실험용 API는 웹에 추가된 강력한 새 기능이므로 보안 컨텍스트에서만 사용할 수 있습니다. 즉, TLS를 염두에 두고 빌드해야 합니다.

사용자 동작 필요

보안 기능으로 navigator.bluetooth.requestDevice를 사용하는 블루투스 기기 검색은 터치 또는 마우스 클릭과 같은 사용자 동작으로 트리거되어야 합니다. pointerup, click, touchend 이벤트를 수신 대기하는 것입니다.

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

코드 살펴보기

Web Bluetooth API는 JavaScript 프로미스에 크게 의존합니다. 이에 익숙하지 않다면 이 프로미스 튜토리얼을 확인하세요. 한 가지 더, () => {}는 ECMAScript 2015 화살표 함수입니다.

블루투스 기기 요청

이 버전의 Web Bluetooth API 사양을 사용하면 중앙 역할에서 실행되는 웹사이트가 BLE 연결을 통해 원격 GATT 서버에 연결할 수 있습니다. 블루투스 4.0 이상을 구현하는 기기 간의 통신을 지원합니다.

웹사이트에서 navigator.bluetooth.requestDevice를 사용하여 근처 기기에 대한 액세스를 요청하면 브라우저에서 기기 선택 도구를 통해 사용자에게 기기 하나를 선택하거나 요청을 취소할 수 있다는 메시지를 표시합니다.

블루투스 기기 사용자 메시지

navigator.bluetooth.requestDevice() 함수는 필터를 정의하는 필수 객체를 사용합니다. 이러한 필터는 일부 공지된 블루투스 GATT 서비스 또는 기기 이름과 일치하는 기기만 반환하는 데 사용됩니다.

서비스 필터

예를 들어 블루투스 GATT 배터리 서비스를 광고하는 블루투스 기기를 요청하려면 다음을 실행하세요.

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

하지만 블루투스 GATT 서비스가 표준화된 블루투스 GATT 서비스 목록에 없는 경우 전체 블루투스 UUID 또는 짧은 16비트 또는 32비트 형식을 제공할 수 있습니다.

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

이름 필터

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

제조업체 데이터 필터

manufacturerData 필터 키를 사용하여 광고 중인 제조업체별 데이터를 기반으로 블루투스 기기를 요청할 수도 있습니다. 이 키는 companyIdentifier라는 필수 블루투스 회사 식별자 키가 포함된 객체의 배열입니다. 데이터 접두사로 시작하는 블루투스 기기에서 제조업체 데이터를 필터링하는 데이터 접두사를 제공할 수도 있습니다. 서비스 필터에 포함되지 않은 모든 서비스에 액세스할 수 있도록 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); });

제조업체 데이터의 일부 패턴과 일치시키기 위해 마스크를 데이터 접두사와 함께 사용할 수도 있습니다. 자세한 내용은 블루투스 데이터 필터 설명을 참고하세요.

제외 필터

navigator.bluetooth.requestDevice()exclusionFilters 옵션을 사용하면 브라우저 선택 도구에서 일부 기기를 제외할 수 있습니다. 더 광범위한 필터와 일치하지만 지원되지 않는 기기를 제외하는 데 사용할 수 있습니다.

// 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 키를 사용하여 근처의 모든 블루투스 기기를 표시할 수 있습니다. 일부 서비스에 액세스하려면 optionalServices 키도 정의해야 합니다. 그렇지 않으면 나중에 액세스하려고 할 때 오류가 발생합니다.

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

블루투스 기기에 연결

이제 BluetoothDevice가 있으니 어떻게 해야 할까요? 서비스 및 특성 정의가 포함된 블루투스 원격 GATT 서버에 연결해 보겠습니다.

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

블루투스 특성 읽기

여기에서 원격 블루투스 장치의 GATT 서버에 연결합니다. 이제 기본 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 특성을 사용하는 경우 전체 블루투스 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);
}

블루투스 특성에 쓰기

블루투스 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 이벤트 리스너를 적절하게 삭제하는 방법을 보여줍니다.

블루투스 기기에서 연결 해제

더 나은 사용자 환경을 제공하려면 연결 끊김 이벤트를 수신 대기하고 사용자가 다시 연결하도록 초대하는 것이 좋습니다.

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()를 호출하여 블루투스 기기에서 웹 앱의 연결을 해제할 수도 있습니다. 이렇게 하면 기존 gattserverdisconnected 이벤트 리스너가 트리거됩니다. 다른 앱이 이미 블루투스 기기와 통신 중인 경우에는 블루투스 기기 통신을 중지하지 않습니다. 자세한 내용은 기기 연결 해제 샘플자동 재연결 샘플을 참고하세요.

블루투스 설명자 읽기 및 쓰기

블루투스 GATT 설명자는 특성 값을 설명하는 속성입니다. 블루투스 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); });

샘플, 데모, Codelab

아래의 모든 웹 블루투스 샘플이 성공적으로 테스트되었습니다. 이 샘플을 최대한 활용하려면 배터리 서비스, 심박수 서비스 또는 건강 온도계 서비스로 BLE 주변기기를 시뮬레이션하는 [BLE 주변기기 시뮬레이터 Android 앱] 을 설치하는 것이 좋습니다.

초급

  • 기기 정보: BLE 기기에서 기본 기기 정보를 검색합니다.
  • Battery Level: 배터리 정보를 광고하는 BLE 기기에서 배터리 정보를 검색합니다.
  • 에너지 재설정: 심박수를 알리는 BLE 기기에서 소모된 에너지를 재설정합니다.
  • 특성 속성 - BLE 기기에서 특정 특성의 모든 속성을 표시합니다.
  • 알림: BLE 기기의 특성 알림을 시작하고 중지합니다.
  • 기기 연결 해제: BLE 기기에 연결한 후 연결을 해제하고 연결 해제에 관한 알림을 받습니다.
  • 특성 가져오기: BLE 기기에서 광고된 서비스의 모든 특성을 가져옵니다.
  • 설명자 가져오기: BLE 기기에서 공지된 서비스의 모든 특성 설명어를 가져옵니다.
  • 제조업체 데이터 필터: BLE 기기에서 제조업체 데이터와 일치하는 기본 기기 정보를 검색합니다.
  • 제외 필터: 기본 제외 필터가 있는 BLE 기기에서 기본 기기 정보를 검색합니다.

여러 작업 결합

  • GAP 특성: BLE 기기의 모든 GAP 특성을 가져옵니다.
  • 기기 정보 특성: BLE 기기의 모든 기기 정보 특성을 가져옵니다.
  • 링크 손실 - BLE 기기의 알림 수준 특성을 설정합니다 (readValue 및 WriteValue).
  • 서비스 및 특성 탐색 - BLE 기기에서 액세스 가능한 모든 기본 서비스와 그 특성을 알아봅니다.
  • Automatic Reconnect: 지수 백오프 알고리즘을 사용하여 연결 해제된 BLE 기기에 다시 연결합니다.
  • Read Characteristic Value Changed: 배터리 수준을 읽고 BLE 기기의 변경 사항에 관한 알림을 받습니다.
  • 설명어 읽기: BLE 기기에서 서비스의 모든 특성 설명어를 읽습니다.
  • 설명어 쓰기: BLE 기기의 설명어 'Characteristic User Description'에 씁니다.

선별된 웹 블루투스 데모공식 웹 블루투스 Codelab도 확인하세요.

라이브러리

  • web-bluetooth-utils는 API에 몇 가지 편의 함수를 추가하는 npm 모듈입니다.
  • Web Bluetooth API shim은 가장 널리 사용되는 Node.js BLE 중앙 모듈인 noble에서 제공됩니다. 이를 통해 WebSocket 서버나 다른 플러그인 없이 noble을 webpack/browserify할 수 있습니다.
  • angular-web-bluetooth는 Web Bluetooth API를 구성하는 데 필요한 모든 상용구를 추상화하는 Angular용 모듈입니다.

도구

  • 웹 블루투스 시작하기는 블루투스 기기와 상호작용을 시작하기 위해 모든 자바스크립트 상용구 코드를 생성하는 간단한 웹 앱입니다. 기기 이름, 서비스, 특성을 입력하고 속성을 정의하기만 하면 됩니다.
  • 이미 블루투스 개발자라면 웹 블루투스 개발자 스튜디오 플러그인에서도 블루투스 기기용 웹 블루투스 자바스크립트 코드를 생성합니다.

Chrome의 about://bluetooth-internals에서 블루투스 내부 페이지를 사용할 수 있으므로 상태, 서비스, 특성, 설명어 등 주변 블루투스 기기에 관한 모든 항목을 검사할 수 있습니다.

Chrome에서 블루투스를 디버깅하는 내부 페이지의 스크린샷
블루투스 기기 디버깅에 관한 Chrome 내부 페이지

블루투스 디버깅이 어려울 수 있으므로 공식 웹 블루투스 버그 신고 방법 페이지를 확인하는 것도 좋습니다.

다음 단계

먼저 브라우저 및 플랫폼 구현 상태를 확인하여 Web Bluetooth API의 어떤 부분이 현재 구현되고 있는지 알아보세요.

아직 불완전하지만 가까운 미래에 예상되는 내용은 다음과 같습니다.

  • 근처 BLE 광고를 검색하는 작업은 navigator.bluetooth.requestLEScan()를 통해 실행됩니다.
  • serviceadded 이벤트는 새로 검색된 블루투스 GATT 서비스를 추적하고 serviceremoved 이벤트는 삭제된 서비스를 추적합니다. 특성 또는 설명어가 블루투스 GATT 서비스에서 추가되거나 삭제되면 새 servicechanged 이벤트가 실행됩니다.

API 지원 표시

Web Bluetooth API를 사용할 계획이신가요? 공개 지원은 Chrome팀에서 기능의 우선순위를 정하는 데 도움이 되며 다른 브라우저 공급업체에 이러한 기능을 지원하는 것이 얼마나 중요한지 알려줍니다.

해시태그 #WebBluetooth를 사용하여 @ChromiumDev로 트윗을 보내고 사용 위치와 방법을 알려주세요.

자료

감사의 말

이 도움말을 리뷰해 주신 Kayce Basques님께 감사드립니다. 히어로 이미지: SparkFun Electronics from Boulder, USA