웹 블루투스로 PLAYBULB 원통 제어

1. 무엇에 관한 것인가요?

IMG_19700101_023537~2~2.jpg

이 Codelab에서는 Web Bluetooth API를 활용하여 자바스크립트만으로 PLAYBULB LED 불꽃 없는 원통을 제어하는 방법을 알아봅니다. 또한 클래스, 화살표 함수, , 프로미스와 같은 JavaScript ES2015 기능도 사용합니다.

학습할 내용

  • JavaScript로 근처 블루투스 기기와 상호작용하는 방법
  • ES2015 클래스, 화살표 함수, 맵, 프로미스 사용 방법

필요한 항목

2. 먼저 재생

실제로 이 Codelab을 시작하기 전에 https://googlecodelabs.github.io/candle-bluetooth에서 앱의 최종 버전을 확인하고 제공되는 PLAYBULB Candle 블루투스 기기를 사용해 보는 것이 좋습니다.

https://www.youtube.com/watch?v=fBCPA9gIxlU에서 제가 색상을 바꾸는 방법을 확인하실 수도 있습니다.

3. 설정

샘플 코드 다운로드

다음 위치에서 zip 파일을 다운로드하여 이 코드에 대한 샘플 코드를 가져올 수 있습니다.

또는 이 git 저장소를 클론합니다.

git clone https://github.com/googlecodelabs/candle-bluetooth.git

소스를 ZIP으로 다운로드한 경우 압축을 풀면 루트 폴더 candle-bluetooth-master이 표시됩니다.

웹 서버 설치 및 확인

자체 웹 서버를 사용해도 되지만 이 Codelab은 Chrome 웹 서버에서 잘 작동하도록 설계되었습니다. 아직 앱을 설치하지 않은 경우 Chrome 웹 스토어에서 설치할 수 있습니다.

Web Server for Chrome 앱을 설치한 후 북마크바에서 앱 바로가기를 클릭하세요.

스크린샷 2016-11-16 4.10.42 PM.png

그 다음 창에서 Web Server 아이콘을 클릭합니다.

9f3c21b2cf6cbfb5.png

그러면 로컬 웹 서버를 구성할 수 있는 대화상자가 표시됩니다.

스크린샷 2016년 11월 16일 오후 3시 40분 47초.png

폴더 선택 버튼을 클릭하고 클론된 (또는 보관 취소된) 저장소의 루트를 선택합니다. 이렇게 하면 웹 서버 대화상자에 강조표시된 URL(웹 서버 URL 섹션)을 통해 진행 중인 작업을 제공할 수 있습니다.

아래와 같이 Options 아래에서 'Automatically show index.html' 옆에 있는 체크박스를 선택합니다.

스크린샷 2016년 11월 16일 오후 3시 40분 56초.png

이제 웹브라우저에서 사이트를 방문하면 (강조표시된 웹 서버 URL을 클릭) 다음과 같은 페이지가 표시됩니다.

스크린샷 2016년 11월 16일 오후 3시 20분 22초.png

Android 휴대전화에서 이 앱이 어떻게 표시되는지 확인하려면 Android에서 원격 디버깅을 사용 설정하고 포트 전달을 설정해야 합니다 (포트 번호는 기본적으로 8887임). 그런 다음 Android 휴대전화에서 새 Chrome 탭을 열어 http://localhost:8887로 이동하면 됩니다.

다음 단계

이 시점에서는 웹 앱이 별다른 작업을 하지 않습니다. 블루투스 지원을 추가해 보겠습니다.

4. 촛불 알아보기

먼저 PLAYBULB Candle Bluetooth 기기에 JavaScript ES2015 클래스를 사용하는 라이브러리를 작성해 보겠습니다.

차분하게 움직이세요. 클래스 문법은 JavaScript에 새로운 객체 지향 상속 모델을 도입하지 않습니다. 아래에서 읽을 수 있는 것처럼 객체를 만들고 상속을 처리하는 훨씬 더 명확한 문법을 제공합니다.

먼저 playbulbCandle.js에서 PlaybulbCandle 클래스를 정의하고 나중에 app.js 파일에서 사용할 수 있는 playbulbCandle 인스턴스를 만들어 보겠습니다.

playbulbCandle.js

(function() {
  'use strict';

  class PlaybulbCandle {
    constructor() {
      this.device = null;
    }
  }

  window.playbulbCandle = new PlaybulbCandle();

})();

근처 블루투스 기기에 대한 액세스를 요청하려면 navigator.bluetooth.requestDevice를 호출해야 합니다. PLAYBULB 원통 기기는 짧은 형식으로 0xFF02라고 하는 상수 블루투스 GATT 서비스 UUID를 (아직 페어링되지 않은 경우) 지속적으로 알리기 때문에 상수를 정의하고 PlaybulbCandle 클래스의 새로운 공개 connect 메서드에 있는 필터 서비스 매개변수에 이를 추가할 수 있습니다.

또한 필요한 경우 나중에 액세스할 수 있도록 내부적으로 BluetoothDevice 객체를 추적합니다. navigator.bluetooth.requestDeviceJavaScript ES2015 프로미스를 반환하므로 then 메서드에서 이 작업을 실행합니다.

playbulbCandle.js

(function() {
  'use strict';

  const CANDLE_SERVICE_UUID = 0xFF02;

  class PlaybulbCandle {
    constructor() {
      this.device = null;
    }
    connect() {
      let options = {filters:[{services:[ CANDLE_SERVICE_UUID ]}]};
      return navigator.bluetooth.requestDevice(options)
      .then(function(device) {
        this.device = device;
      }.bind(this)); 
    }
  }

  window.playbulbCandle = new PlaybulbCandle();

})();

보안 기능으로 인해 navigator.bluetooth.requestDevice로 근처 블루투스 기기를 찾는 기능은 터치나 마우스 클릭과 같은 사용자 동작을 통해 호출해야 합니다. 따라서 사용자가 '연결' 버튼을 클릭할 때 connect 메서드를 호출합니다. app.js 파일의 다음 버튼을 사용하세요.

app.js

document.querySelector('#connect').addEventListener('click', function(event) {
  document.querySelector('#state').classList.add('connecting');
  playbulbCandle.connect()
  .then(function() {
    console.log(playbulbCandle.device);
    document.querySelector('#state').classList.remove('connecting');
    document.querySelector('#state').classList.add('connected');
  })
  .catch(function(error) {
    console.error('Argh!', error);
  });
});

앱 실행

이때 웹 서버 앱에서 강조 표시된 웹 서버 URL을 클릭하여 웹브라우저에서 사이트를 방문하거나 기존 페이지를 새로고침합니다. 녹색 '연결'을 클릭합니다. 버튼을 클릭하고 선택기에서 기기를 선택하고 Ctrl + Shift + J 단축키를 사용하여 즐겨 사용하는 개발 도구 콘솔을 열고 BluetoothDevice 객체가 로깅된 것을 확인할 수 있습니다.

스크린샷 2016년 11월 16일 오후 3시 27분 12초.png

블루투스가 꺼져 있거나 PLAYBULB Candle 블루투스 기기가 꺼져 있으면 오류가 발생할 수 있습니다. 이 경우 사용 설정하고 다시 진행하세요.

필수 보너스

잘 모르지만 이 코드에 function() {}가 이미 너무 많이 표시되어 있습니다. 대신 () => {} JavaScript ES2015 화살표 함수로 전환해 보겠습니다. 생명을 구하는 절대적인 수단입니다. 익명 함수가 지닌 아름다움을 누릴 수 있으며, 바인딩의 슬픔도 존재하지 않습니다.

playbulbCandle.js

(function() {
  'use strict';

  const CANDLE_SERVICE_UUID = 0xFF02;

  class PlaybulbCandle {
    constructor() {
      this.device = null;
    }
    connect() {
      let options = {filters:[{services:[ CANDLE_SERVICE_UUID ]}]};
      return navigator.bluetooth.requestDevice(options)
      .then(device => {
        this.device = device;
      }); 
    }
  }

  window.playbulbCandle = new PlaybulbCandle();

})();

app.js

document.querySelector('#connect').addEventListener('click', event => {
  playbulbCandle.connect()
  .then(() => {
    console.log(playbulbCandle.device);
    document.querySelector('#state').classList.remove('connecting');
    document.querySelector('#state').classList.add('connected');
  })
  .catch(error => {
    console.error('Argh!', error);
  });
});

다음 단계

- 그럼... 이 촛불과 대화할 수 있을까요? 아니면 뭐라고 할까요?

- 물론입니다. 다음 단계로 이동하겠습니다

자주 묻는 질문(FAQ)

5. 읽기

이제 navigator.bluetooth.requestDevice의 프로미스에서 BluetoothDevice가 반환되었으므로 이제 무엇을 해야 할까요? device.gatt.connect()를 호출하여 블루투스 서비스와 특성 정의를 보유한 블루투스 원격 GATT 서버에 연결해 보겠습니다.

playbulbCandle.js

  class PlaybulbCandle {
    constructor() {
      this.device = null;
    }
    connect() {
      let options = {filters:[{services:[ CANDLE_SERVICE_UUID ]}]};
      return navigator.bluetooth.requestDevice(options)
      .then(device => {
        this.device = device;
        return device.gatt.connect();
      });
    }
  }

기기 이름 읽기

여기서 PLAYBULB Candle Bluetooth 기기의 GATT 서버에 연결됩니다. 이제 기본 GATT 서비스 (이전에는 0xFF02로 공지됨)를 가져와서 이 서비스에 속한 기기 이름 특성 (0xFFFF)을 읽으려고 합니다. 이렇게 하려면 새 메서드 getDeviceNamePlaybulbCandle 클래스에 추가하고 device.gatt.getPrimaryServiceservice.getCharacteristic를 사용하면 됩니다. characteristic.readValue 메서드는 실제로 DataView를 반환합니다. 여기서는 TextDecoder로 간단히 디코딩합니다.

playbulbCandle.js

  const CANDLE_DEVICE_NAME_UUID = 0xFFFF;

  ...

    getDeviceName() {
      return this.device.gatt.getPrimaryService(CANDLE_SERVICE_UUID)
      .then(service => service.getCharacteristic(CANDLE_DEVICE_NAME_UUID))
      .then(characteristic => characteristic.readValue())
      .then(data => {
        let decoder = new TextDecoder('utf-8');
        return decoder.decode(data);
      });
    }

연결되고 기기 이름을 표시한 후 playbulbCandle.getDeviceName를 호출하여 이를 app.js에 추가해 보겠습니다.

app.js

document.querySelector('#connect').addEventListener('click', event => {
  playbulbCandle.connect()
  .then(() => {
    console.log(playbulbCandle.device);
    document.querySelector('#state').classList.remove('connecting');
    document.querySelector('#state').classList.add('connected');
    return playbulbCandle.getDeviceName().then(handleDeviceName);
  })
  .catch(error => {
    console.error('Argh!', error);
  });
});

function handleDeviceName(deviceName) {
  document.querySelector('#deviceName').value = deviceName;
}

이때 웹 서버 앱에서 강조 표시된 웹 서버 URL을 클릭하여 웹브라우저에서 사이트를 방문하거나 기존 페이지를 새로고침합니다. PLAYBULB 원통이 켜져 있는지 확인한 다음 '연결'을 클릭합니다. 색상 선택 도구 아래에 기기 이름이 표시됩니다.

스크린샷 2016년 11월 16일 오후 3시 29분 21초.png

배터리 잔량 읽기

PLAYBULB 원통 블루투스 기기에는 기기의 배터리 잔량이 포함된 표준 배터리 수준 블루투스 특성도 있습니다. 즉, 블루투스 GATT 서비스 UUID에는 battery_service, 블루투스 GATT 특성 UUID에는 battery_level와 같은 표준 이름을 사용할 수 있습니다.

PlaybulbCandle 클래스에 새 getBatteryLevel 메서드를 추가하고 배터리 잔량을 퍼센트로 읽어 보겠습니다.

playbulbCandle.js

    getBatteryLevel() {
      return this.device.gatt.getPrimaryService('battery_service')
      .then(service => service.getCharacteristic('battery_level'))
      .then(characteristic => characteristic.readValue())
      .then(data => data.getUint8(0));
    }

또한 optionalServices 키에 배터리 서비스를 포함하도록 options JavaScript 객체를 업데이트해야 합니다. 이 객체는 PLAYBULB Candle 블루투스 기기에서 광고하지 않지만 이 객체에 액세스하려면 반드시 필요합니다.

playbulbCandle.js

      let options = {filters:[{services:[ CANDLE_SERVICE_UUID ]}],
                     optionalServices: ['battery_service']};
      return navigator.bluetooth.requestDevice(options)

이전과 마찬가지로 기기 이름이 있으면 playbulbCandle.getBatteryLevel를 호출하여 app.js에 연결하고 배터리 잔량을 표시해 보겠습니다.

app.js

document.querySelector('#connect').addEventListener('click', event => {
  playbulbCandle.connect()
  .then(() => {
    console.log(playbulbCandle.device);
    document.querySelector('#state').classList.remove('connecting');
    document.querySelector('#state').classList.add('connected');
    return playbulbCandle.getDeviceName().then(handleDeviceName)
    .then(() => playbulbCandle.getBatteryLevel().then(handleBatteryLevel));
  })
  .catch(error => {
    console.error('Argh!', error);
  });
});

function handleDeviceName(deviceName) {
  document.querySelector('#deviceName').value = deviceName;
}

function handleBatteryLevel(batteryLevel) {
  document.querySelector('#batteryLevel').textContent = batteryLevel + '%';
}

이때 웹 서버 앱에서 강조 표시된 웹 서버 URL을 클릭하여 웹브라우저에서 사이트를 방문하거나 기존 페이지를 새로고침합니다. '연결'을 클릭합니다. 버튼을 누르면 기기 이름과 배터리 잔량이 모두 표시됩니다.

스크린샷 2016년 11월 16일 오후 3시 29분 21초.png

다음 단계

- 이 전구의 색상을 변경하려면 어떻게 해야 하나요? 그래서 이 자리에 모셨습니다.

- 맹세할 수 있겠지만...

자주 묻는 질문(FAQ)

6. 색상 변경

색상을 변경하는 것은 0xFF02로 알려진 기본 GATT 서비스의 블루투스 특성 (0xFFFC)에 특정 명령어 세트를 작성하는 것만큼 쉽습니다. 예를 들어 PLAYBULB Candle을 빨간색으로 바꾸면 [0x00, 255, 0, 0]와 같은 부호 없는 8비트 정수 배열이 작성됩니다. 여기서 0x00는 흰색 채도이고 255, 0, 0는 각각 빨간색, 녹색, 파란색 값입니다.

characteristic.writeValue를 사용하여 실제로 PlaybulbCandle 클래스의 새 setColor 공개 메서드에서 블루투스 특성에 일부 데이터를 씁니다. 또한 프로미스가 처리될 때 실제 빨간색, 녹색, 파란색 값을 반환하여 나중에 app.js에서 사용할 수 있습니다.

playbulbCandle.js

  const CANDLE_COLOR_UUID = 0xFFFC;

  ...

    setColor(r, g, b) {
      let data = new Uint8Array([0x00, r, g, b]);
      return this.device.gatt.getPrimaryService(CANDLE_SERVICE_UUID)
      .then(service => service.getCharacteristic(CANDLE_COLOR_UUID))
      .then(characteristic => characteristic.writeValue(data))
      .then(() => [r,g,b]);
    }

'효과 없음' 메시지가 표시될 때 playbulbCandle.setColor를 호출하도록 app.jschangeColor 함수를 업데이트해 보겠습니다. 라디오 버튼이 선택되어 있는지 확인합니다. 전역 r, g, b 색상 변수는 사용자가 색상 선택 도구 캔버스를 클릭할 때 이미 설정되어 있습니다.

app.js

function changeColor() {
  var effect = document.querySelector('[name="effectSwitch"]:checked').id;
  if (effect === 'noEffect') {
    playbulbCandle.setColor(r, g, b).then(onColorChanged);
  }
}

이때 웹 서버 앱에서 강조 표시된 웹 서버 URL을 클릭하여 웹브라우저에서 사이트를 방문하거나 기존 페이지를 새로고침합니다. '연결'을 클릭합니다. 버튼을 클릭하고 색상 선택기를 클릭하여 PLAYBULB 원통의 색상을 원하는 횟수만큼 변경합니다.

스크린샷 2016년 11월 16일 오후 3시 31분 37초.png

모어 캔들 효과

이전에 이미 촛불을 켜본 적이 있다면 불이 정전기가 아님을 알 수 있습니다. 다행히 0xFF02라고 알려진 기본 GATT 서비스에는 사용자가 캔들 효과를 설정할 수 있는 또 다른 블루투스 특성 (0xFFFB)이 있습니다.

'원통 효과' 설정 예를 들어 [0x00, r, g, b, 0x04, 0x00, 0x01, 0x00]를 작성하면 됩니다. '깜박임 효과'를 설정하여 [0x00, r, g, b, 0x00, 0x00, 0x1F, 0x00]를 사용합니다.

PlaybulbCandle 클래스에 setCandleEffectColor 메서드와 setFlashingColor 메서드를 추가해 보겠습니다.

playbulbCandle.js

  const CANDLE_EFFECT_UUID = 0xFFFB;

  ...

    setCandleEffectColor(r, g, b) {
      let data = new Uint8Array([0x00, r, g, b, 0x04, 0x00, 0x01, 0x00]);
      return this.device.gatt.getPrimaryService(CANDLE_SERVICE_UUID)
      .then(service => service.getCharacteristic(CANDLE_EFFECT_UUID))
      .then(characteristic => characteristic.writeValue(data))
      .then(() => [r,g,b]);
    }
    setFlashingColor(r, g, b) {
      let data = new Uint8Array([0x00, r, g, b, 0x00, 0x00, 0x1F, 0x00]);
      return this.device.gatt.getPrimaryService(CANDLE_SERVICE_UUID)
      .then(service => service.getCharacteristic(CANDLE_EFFECT_UUID))
      .then(characteristic => characteristic.writeValue(data))
      .then(() => [r,g,b]);
    }

'원통 효과'가 생성될 때 playbulbCandle.setCandleEffectColor를 호출하도록 app.jschangeColor 함수를 업데이트하겠습니다. '플래시' 버튼이 선택되어 있으면 playbulbCandle.setFlashingColor 라디오 버튼이 선택되어 있는지 확인합니다. 이번에는 괜찮으시다면 switch을(를) 사용하겠습니다.

app.js

function changeColor() {
  var effect = document.querySelector('[name="effectSwitch"]:checked').id;
  switch(effect) {
    case 'noEffect':
      playbulbCandle.setColor(r, g, b).then(onColorChanged);
      break;
    case 'candleEffect':
      playbulbCandle.setCandleEffectColor(r, g, b).then(onColorChanged);
      break;
    case 'flashing':
      playbulbCandle.setFlashingColor(r, g, b).then(onColorChanged);
      break;
  }
}

이때 웹 서버 앱에서 강조 표시된 웹 서버 URL을 클릭하여 웹브라우저에서 사이트를 방문하거나 기존 페이지를 새로고침합니다. '연결'을 클릭합니다. 버튼을 클릭하고 원통 및 플래싱 효과를 사용하여 재생하세요.

스크린샷 2016년 11월 16일 오후 3시 33분 23초.png

다음 단계

- 그게 다야? 3가지 불량한 촛불 효과? 제가 이 자리에 모인 이유가 여기에 있나요?

- 더 많지만 이번에는 혼자 해내야 합니다.

7. 한 걸음 더 나아가기

자, 여기 있습니다! 거의 끝났다고 생각할 수도 있지만 앱이 아직 끝나지 않았습니다. 이 Codelab에서 복사하여 붙여넣은 내용을 실제로 이해했는지 확인해 보겠습니다. 이 앱을 빛내기 위해 직접 할 일은 다음과 같습니다.

누락된 효과 추가

다음은 누락된 효과에 대한 데이터입니다.

  • 깜빡임: [0x00, r, g, b, 0x01, 0x00, 0x09, 0x00] (r, g, b 값을 조정해야 할 수 있음)
  • 무지개: [0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00] (간질병 환자라면 피해야 할 것 같음)
  • 무지개색으로 흐려짐: [0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x26, 0x00]

이는 기본적으로 새 setPulseColor, setRainbow, setRainbowFade 메서드를 PlaybulbCandle 클래스에 추가하고 changeColor에서 호출하는 것을 의미합니다.

'효과 없음' 문제 해결

이미 알고 계시겠지만 옵션이 진행 중인 효과를 재설정하지 않으므로 사소한 문제이지만 여전히 그렇습니다. 이 문제를 해결해 보겠습니다. setColor 메서드에서 먼저 새 클래스 변수 _isEffectSet를 통해 효과가 진행되는지 확인하고 true이면 효과를 사용 중지한 후 다음 데이터로 새 색상을 설정해야 합니다. [0x00, r, g, b, 0x05, 0x00, 0x01, 0x00]

기기 이름 쓰기

이건 쉽네! 맞춤 기기 이름은 이전의 블루투스 기기 이름 특성에 쓰는 것만큼 간단합니다. TextEncoder encode 메서드를 사용하여 기기 이름이 포함된 Uint8Array를 가져오는 것이 좋습니다.

그런 다음 '입력'을 document.querySelector('#deviceName')eventListener하고 playbulbCandle.setDeviceName를 호출하여 단순성을 유지합니다.

저는 제 이름을 PLAY💡 CANDLE로 지정했습니다.

8. 작업이 끝났습니다.

학습한 내용

  • JavaScript로 근처 블루투스 기기와 상호작용하는 방법
  • ES2015 클래스, 화살표 함수, 맵, 프로미스 사용 방법

다음 단계