CHIRIMEN for Raspberry Pi 3 チュートリアル 5. WebBluetooth 編


**本記事は、[CHIRIMEN for Raspberry Pi Ver.2018/03/08](https://github.com/chirimen-oh/chirimen-raspi3/releases/tag/20180308)以前のVersionに対応しています。
新しいCHIRIMEN for Raspberry Piをお使いの方は、[新しいCHIRIMENチュートリアル](https://tutorial.chirimen.org/)を参照ください。**

これは Raspberry Pi 3 上で動作する IoT プラットフォーム「CHIRIMEN for Raspberry Pi 3」で WebBluetooth を使用してライトを制御するサンプルです。USBマイクから音が入るとライトが点灯する、または測距センサで何かが近づいたらライトが点灯する、という動作をさせています。

## ソースコード
ソフトウェア本体は GitHub 上のリポジトリにあります。  
[https://github.com/g200kg/chirimen-webbluetooth](https://github.com/g200kg/chirimen-webbluetooth)

## ライブデモ
オンラインで実際に動作するサンプルは下のリンクで公開されています。CHIRIMEN for RasPi 3 上で全ての機能を動かすには下の部品を揃える必要がありますが、もし PLAYBULB (sphere または candle)をお持ちなら(BLE対応の)ノートPCからでもマイク周りの動作を試す事ができます。  

[ライブデモ](https://g200kg.github.io/chirimen-webbluetooth/)

## 必要なもの
* CHIRIMEN for Raspberry Pi が動作する Raspberry Pi 3 Model B
* BLE制御できる ライト、PLAYBULB sphere (または PLAYBULB candle)
* USB マイク
* 測距センサ (GP2Y0E03)  

![必要なもの](https://g200kg.github.io/chirimen-webbluetooth/images/bledemo1.jpeg)

## 準備
* CHIRIMEN for Raspberry Pi の基本的な動かし方については、「[CHIRIMEN for Raspberry Pi 3 Hello World](https://qiita.com/tadfmac/items/82817476615fdc7394b3)」の通りにできているものとします。
また、測距センサ(GP2Y0E03)は「[CHIRIMEN for Raspberry Pi 3 チュートリアル 3. I2C　応用編（その他のセンサー）](https://qiita.com/tadfmac/items/b17d8c6a35b31c495a36)」の通りに動作しているものとします。

*  現在の所、Chromium のデフォルト状態では WebBluetooth は有効になっていないため、まず WebBluetoothを使えるようにブラウザの設定を変えます。  
  * ブラウザで chrome://flags にアクセスして試験運用機能の画面を開きます
  * "Experimental Web Platform features" という項目を探し、 [有効] に切り替えてブラウザを再起動します ("Experimental" で始まる項目は複数あるので間違えない事)  
  * WebBluetoothが使えるようになっているかの確認は、F12 キーでコンソールを出し、
"> navigator.bluetooth" と入力して
"▶ Bluetooth {}"
が返ってくればOKです。"undefined" になる場合は WebBluetooth が有効になっていません。  


* マイクの設定  
  * chrome://settings にアクセスして Chromium の設定画面を開きます
  * [詳細設定] - [コンテンツの設定] - [マイク]　に進みます
  * USBマイクを接続します
  * 最初の項目(何も設定していなければ、[既定]になっています) で使用するマイクを選択します。この画面を出した状態でUSBマイクを接続した時に新たに現れる項目が対象のマイクです。名前は使用するマイクによって異なりますが、ここで使用している超小型マイクでは、[USB PnP Sound Device, USB Audio-Default Audio Device]となっています。

## サンプルプログラムの起動

実際に Web Bluetooth を使うサンプルプログラムは、https://g200kg.github.io/chirimen-webbluetooth/ に置いてあります。ブラウザでアクセスすると次の画面になります。この時、マイクの使用許可のダイアログがでたら[許可]を選択してください。  

![images/bledemo2.png](https://g200kg.github.io/chirimen-webbluetooth/images/bledemo2.png)

### Bluetooth ライトの基本的な制御

bluetooth デモを起動して [BLE Connect]を押すと BLE デバイスを選択するダイアログが出てきます。ここで PLAYBULB (sphere または candle)を選択して [ペア設定] を押す事で BLE デバイスと接続されます。
接続ができた状態で [Off]、[Red]、[Green]、[Blue]、[White]の各ボタンを押すと PLAYBULB の色が指定の色に変わります。  

![images/bledemo3.png](https://g200kg.github.io/chirimen-webbluetooth/images/bledemo3.png)  

### マイクからの制御

マイクのボタンを押して[On]状態にすると音に反応するようになります。音の周波数帯域を3つに分けてそれぞれ色の RGB に割り当てていますのでマイクで拾う音によって PLAYBULB の色が変わります。  

![images/bledemo4.png](https://g200kg.github.io/chirimen-webbluetooth/images/bledemo4.png)  

実際の動作状況の動画がありますので見てみてください。  

[![blemov](https://g200kg.github.io/chirimen-webbluetooth/images/blemov.png)](https://g200kg.github.io/chirimen-webbluetooth/images/blemov.mp4)

### 測距センサからの制御  

測距センサーのボタンを[On]状態にすると、測距センサーで検出した障害物までの距離によって PLAYBULB の色が変わります。この測距センサーの検出範囲は4～50cm程度ですので、距離が近くなるにつれて色が青⇒緑⇒赤と変化するようになっています。  

![images/bledemo5.jpeg](https://g200kg.github.io/chirimen-webbluetooth/images/bledemo5.jpeg)

## コードの説明

#### Web BlueTooth API
BLEデバイス、PLAYBULB を制御する部分は class Playbulb にまとめてあります。
ブラウザが WebBluetooth をサポートしていれば navigator.bluetooth が存在しますので、まず

```js
navigator.bluetooth.requestDevice(options)
```

で周囲のBLEデバイスのスキャンを行います。この時、options には使いたいサービスのIDを指定する事で、そのサービスを持っているデバイスだけがスキャンされます。この時点で Bluetooth のペア設定ダイアログが表示され、ユーザーがデバイスを選択する事で requestDevice の promise が解決され、BLE デバイスを指すオブジェクトが得られます。

次にBLEデバイスオブジェクトに対して

```js
device.gatt.connect()
```

を使用して、デバイスの機能との通信の枠組みである GATT（Generic Attribute Profile）サーバーに接続します。
GATTサーバーとの接続ができれば、実際に PLAYBULB の色を設定する部分は setColor() という関数内にあります。

```js
let data = new Uint8Array([0x00, r, g, b]);
return this.device.gatt.getPrimaryService(bledevices[this.device.name].serviceId)
  .then(service => service.getCharacteristic(COLOR_UUID))
  .then(characteristic => characteristic.writeValue(data))
  .then(() => [r,g,b]);
```

RGBの値(各0-255)を PLAYBULB に設定するには、getPrimaryService(serviceId) でサービスを取得し、getCharacteristic(COLUR_UUID) で個別の属性(色設定)を取得し、characteristic.writeValue(data) でそこに値を書き込む、という手順になります。

#### getUserMedia / Web Audio API
PLAYBULB の色の基本的な設定ができるようになれば、後はマイクで拾う音で制御するなり、測距センサーから制御するなり自由ですが、ここでは getUserMedia でマイクから拾った音を Web Audio API の analyser で周波数分割し、RGB値に変換しています。
ここで使用する Media Capture 関係の API や Web Audio API 等はブラウザの API としては新しいものですので、仕様の変更もまだ頻繁に行われています。内容が古くなって動作しないサンプルなども Web 上では散見されますので、最新の仕様は W3C のEditor's Draft で確認したい所です。

さて、まず class Microphone で getUserMedia と Web Audio API を使ってマイクからの音を
拾って周波数領域への変換を行います。
Web Audioノードの接続としては、  

 MediaStreamSourceNode(getUserMedia) => AnalyserNode => GainNode => DestinationNode  

 となっています。  

 GainNode の gain が 0 ですので、結局、マイクで拾った音そのものは外に出さずにミュートしてしまっているのですが、Web Audio API の仕様上、最終的に Destination まで繋がっていないノードはノードそのものが動作を止めてしまうという事になっていますので、こういう形を取っています。もちろん gain を 0 よりも大きい値にすれば、拾った音をスピーカーから出してモニターするようにする事もできますが、ハウリングを起こしやすいのでご注意ください。  

new AnalyserNode(this.audioctx,{smoothingTimeConstant:0.5}) や new GainNode(this.audioctx,{gain:0}) という書き方は Web Audio API の audioctx.createXXX() に代わる新しい書き方になりますが、ノードの生成と同時にプロパティの初期値を指定できるというメリットがあります。  

また、getUserMedia() が mediaDevices.getUserMedia() になっているのも割合新しい書き方ですね。以前は navigator.getUserMedia() だったのですが、このあたりの最新仕様も詳細は W3C の Editor's Draft で確認してください。

さて、マイクの音を実際に周波数帯域に分割する動作をしているのは AnalyserNode です。AnalyserNode はパラメータが多いのですが、最初からいわゆるスペクトラムアナライザー(スペアナ)的な表示に使えるようにデフォルト値がほぼチューニングされた状態になっています。ここではノードの生成時に smoothingTimeConstant だけ少しいじっていますが、これは(スペアナの)メーターの戻りのスピードを調整するものです。今回は BLE 経由のライトを光らせますのでやや遅めの設定になっています。

AnalyserNode からは getByteFrequencyData(array) で現時点の周波数帯域ごとの強度(マグニチュード)のデータが取れます(getLevel()関数内)。結果は引数の array に格納されますが、array のサイズが小さい場合は周波数が低い方からarray のサイズ分だけを取得する事になります。

```js
class Microphone {
  constructor(){
    this.audioctx=null;
    this.audioctx=new AudioContext();
    this.analyser=new AnalyserNode(this.audioctx,{smoothingTimeConstant:0.5});
    this.gain=new GainNode(this.audioctx,{gain:0});
    navigator.mediaDevices.getUserMedia({audio:true,video:false})
    .then(
      (strm)=>{
        let audio = this.audioctx.createMediaStreamSource(strm);
        audio.connect(this.analyser).connect(this.gain).connect(this.audioctx.destination);
      },
      (err)=>{
        console.log(err);
      }
    );
    this.run=-1;
  }
  toggle(){
    if(this.run<0){
      this.audioctx.resume();
      document.getElementById("micbtn").innerHTML="On";
      this.run=1;
    }
    else {
      this.run^=1;
      let btn=document.getElementById("micbtn");
      btn.innerHTML=this.run?"On":"Off";
    }
  }
  getLevel(array){
    if(this.analyser)
      this.analyser.getByteFrequencyData(array);
  }
}
```

AnalyserNode から取った周波数データは FFT のポイント数がデフォルトで 2048 ですので、44.1kHz の音声信号に対して

44.1kHz / 2048 ≒ 21.5Hz  

毎の強度データになります。単位はdB(デシベル)ですが通常の音声データに対して Uint8Array で各点 0-255 の範囲に程よく収まるようにデフォルトのパラメータで調整されています。今回のサンプルではこれを33点毎にまとめて平均したものを3組作っています。

```js
if(mic.run>0){
  mic.getLevel(arr);
  let lev=[0,0,0];
  for(let i=0;i<33;++i){
    lev[0]+=arr[1+i]/33;
    lev[1]+=arr[34+i]/33;
    lev[2]+=arr[67+i]/33;
  }
  for(let i=0;i<3;++i)
    meter[i].value=lev[i]/256;
  playbulb.setColor(lev[0],lev[1],lev[2]);
}
```

つまり

|帯域  |FFTポイント| 周波数帯域            |ライト色|
|:----:|:--------:|:--------------------:|:-----:|
|Bass  |   1-33   |  21.5 - 709.5 Hz     |   赤  |
|Middle|   34-66  |  731  - 1419  Hz     |   緑  |
|Treble|   67-99  |  1440.5 - 2128.5 Hz  |   青  |  


という対応になります。この辺の取り決めは適当に決めたものではありますが、大体人間の声の母音が含む倍音のあたりに相当しますので「あ」「い」「う」「え」「お」の各母音の違いに割合良く反応するようになっています。  

#### 測距センサー  

同様に測距センサーの検出値から色を制御する部分は次の通りです。距離 dist は cm 単位で 50-4 程度ですので、それを三分割して、B => G => R の順にピークが来るように変換しています。

```js
let dist=distance.get();
let light=((dist==null)?0:Math.max(0,60-dist))/60;
if(dist==null)
  document.getElementById("distancedisplay").innerHTML="Out of Range";
else
  document.getElementById("distancedisplay").innerHTML=dist+" cm";
meter[3].value=light;
let r=(light>2/3)?(light-2/3)*3:0;
let g=(light>2/3)?(1-(light-2/3)*3):(light>1/3)?(light-1/3)*3:0;
let b=(light>2/3)?0:(light>1/3)?(1-(light-1/3)*3):light*3;
playbulb.setColor(r*255,g*255,b*255);
```

#### UUID について
 PLAYBULB を WebBluetooth で制御するサンプルの元ネタは google codelabs の次の記事にあります。

 [https://codelabs.developers.google.com/codelabs/candle-bluetooth/#0](https://codelabs.developers.google.com/codelabs/candle-bluetooth/#0)

ただしこの記事で扱っているのは PLAYBULB candle というモデルになりますが、PLAYBULB には幾つかの種類があってそれぞれサービスIDが異なるようです。ネット上を探すと各モデルのサービスIDらしきものが提示されていたりするのですが、どれも公式の仕様ではなくユーザーが解析した結果のようです。今の所以下の定義通りの各デバイスのサービスIDおよび色設定のキャラクタリスティックIDを使用しています。

```js
const bledevices={
  "PLAYBULB sphere":{serviceId:0xFF0F},
  "PLAYBULB CANDLE":{serviceId:0xFF02},
};

const COLOR_UUID = 0xFFFC;
```

このサンプルでは、PLAYBULB sphere と PLAYBULB candle に対応しているはずですが、やや情報が錯綜しており、同じモデルでもリビジョンによってIDが異なるのでは、等の意見もあるようですので、もし手持ちの PLAYBULB で思うように動作しないケースがあれば、その点も留意ください。

また、UUID は通常128ビット長ですが、ここで扱っているサービス ID は UUID と言いつつ 0xFF02 等、16ビット長しかありません。これは BLE の仕様で短縮 UUID というもので、
0000xxxx-0000-1000-8000-00805F9B34FB　の xxxx の部分だけを表す形式です。ただし短縮 UUID を使えるのは Bluetooth SIG で承認されたものだけですという事なのですが、Bluetooth SIG の 16ビットUUID の一覧を見ても PLAYBULB の ID は見つかりませんので、PLAYBULB のBLE実装はまだ実験的なレベルのものなのかも知れません。

## 参考  
[CHIRIMEN for Raspberry Pi 3 Hello World](https://qiita.com/tadfmac/items/82817476615fdc7394b3)
[CHIRIMEN for Raspberry Pi 3 チュートリアル 3. I2C　応用編（その他のセンサー）](https://qiita.com/tadfmac/items/b17d8c6a35b31c495a36)
[codelabs : Control a PLAYBULB candle with Web Bluetooth](https://codelabs.developers.google.com/codelabs/candle-bluetooth/#0)
[W3C : Media Capture and Streams](https://www.w3.org/TR/mediacapture-streams/)
[W3C : Web Audio API](https://webaudio.github.io/web-audio-api/)
[Bluetooth SIG : メンバー向け 16 ビット UUID](https://www.bluetooth.com/ja-jp/specifications/assigned-numbers/16-bit-uuids-for-members)

### CHIRIMEN for Raspberry Pi 3 チュートリアル一覧

- [CHIRIMEN for Raspberry Pi 3 Hello World](https://qiita.com/tadfmac/items/82817476615fdc7394b3)
- [CHIRIMEN for Raspberry Pi 3 チュートリアル 1. GPIO編](https://qiita.com/tadfmac/items/ebd01cfe46e30de70f3d)
- [CHIRIMEN for Raspberry Pi 3 チュートリアル 2. I2C　基本編（ADT7410温度センサー）](https://qiita.com/tadfmac/items/36d5467f79b6fd3114fb)
- [CHIRIMEN for Raspberry Pi 3 チュートリアル 3. I2C　応用編（その他のセンサー）](https://qiita.com/tadfmac/items/b17d8c6a35b31c495a36)
- [CHIRIMEN for Raspberry Pi 3 チュートリアル 4. GPIO/I2C編　まとめ](https://qiita.com/tadfmac/items/d627f8d2fec3c5f8711b)
- [CHIRIMEN for Raspberry Pi 3 チュートリアル 5. WebBluetooth編](https://qiita.com/g200kg/items/28b3cc8c058bb49673a2)