ESP32＋BLE＋Web bluetoothでWeb上にデータをリアルタイム表示

# はじめに
ESP32-DevkitCが手元にあるので、ESP32のBLEとWeb bluetoothを使って、Web上にデータをリアルタイム表示させてみました。

マイコン（ESP32）からのデータ送信は下記を参考にしました。

- [Nefry BTからブラウザにWeb Bluetoothでデータを表示する方法](https://qiita.com/umi_kappa/items/dd3d7a27cf714971406e)

- [Nefry BT（ESP32）からBLEでNode.jsにデータを送ってみよう]
(https://dotstud.io/blog/nefrybt-ble-bluetooth-peripheral/)


# つかったもの

## ハードウェア
- ESP32-DevkitC
  - 今回はセンサを使わずにランダム数を送信します
  
## ソフトウェア
- Web bluetooth
- chart.js
  - いい感じのグラフを生成してくれるjs
- [chartjs-plugin-streaming.js](https://nagix.github.io/chartjs-plugin-streaming/ja/)
  - リアルタイムストリーミングデータ向けのchart.jsのプラグインで、ストリーミングチャートを簡単に生成できる
  - リンク先のサンプルプログラムをがっつり参考にした
- moment.js
  - 日付をいい感じに使えるようにしてくれjs

# プログラム
## Esp32
```c++


#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

BLECharacteristic *pCharacteristic;
bool deviceConnected = false;
uint8_t value = 0;
int j = 0;
int len = 0;
char buf[100];
char buf_serialinput[100];

// See the following for generating UUIDs:
// https://www.uuidgenerator.net/

#define SERVICE_UUID        "d5875408-fa51-4763-a75d-7d33cecebc31"
#define CHARACTERISTIC_UUID "a4f01d8c-a037-43b6-9050-1876a8c23584"

class MyServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
      deviceConnected = true;
    };

    void onDisconnect(BLEServer* pServer) {
      deviceConnected = false;
    }
};

class MyCallbacks: public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristic) {
      std::string j = pCharacteristic->getValue();
      int len = j.length();
      Serial.println(len);
      Serial.println(j.c_str());
    }
};
MyCallbacks myCallbacks;

void setup() {
  Serial.begin(115200);

  // Create the BLE Device
  BLEDevice::init("NefryBT");

  // Create the BLE Server
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());

  // Create the BLE Service
  BLEService *pService = pServer->createService(SERVICE_UUID);

  // Create a BLE Characteristic
  pCharacteristic = pService->createCharacteristic(
                      CHARACTERISTIC_UUID,
                      BLECharacteristic::PROPERTY_READ   |
                      BLECharacteristic::PROPERTY_WRITE  |
                      BLECharacteristic::PROPERTY_NOTIFY |
                      BLECharacteristic::PROPERTY_INDICATE
                    );

  // https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.descriptor.gatt.client_characteristic_configuration.xml
  // Create a BLE Descriptor
  pCharacteristic->setCallbacks(&myCallbacks);
  pCharacteristic->addDescriptor(new BLE2902());

  // Start the service
  pService->start();

  // Start advertising
  pServer->getAdvertising()->start();
  Serial.println("Waiting a client connection to notify...");
}

void loop() {
  int index = 0;
  bool hasData = false;
  
  if (deviceConnected) {
    char buffer[32];
    int random_num = random(255);
    sprintf(buffer, "%d", random_num);
    Serial.printf("%d\n", random_num);
    pCharacteristic->setValue(buffer);
    pCharacteristic->notify();
    //pCharacteristic->indicate();
    value++;
  }  
  delay(2000);
}

```

## WEB側
### HTML
```html


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">

    <link rel="stylesheet" href="css/flat-ui.min.css" />
	<link rel="stylesheet" href="css/index.css" />

    <script src="https://cdn.jsdelivr.net/npm/moment@2.24.0/min/moment.min.js"></script>
	<script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0"></script>
	<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-streaming@1.8.0"></script>
    
	<script src="js/main.js"></script>

    <title>chart.js</title>
</head>
<body>
    <div id="search-button" class="show">
        <div class="view-inner">
            <div class="btn btn-block btn-lg btn-primary">SEARCH</div>
        </div>
    </div>
    
    <canvas id="myChart" width="400" height="350"></canvas>
</body>
</html>

```

### JavaScript

```javascript


// UUID
const SERVICE_UUID = "d5875408-fa51-4763-a75d-7d33cecebc31";
const RX_CHARACTERISTIC_UUID = "a4f01d8c-a037-43b6-9050-1876a8c23584";
const TX_CHARACTERISTIC_UUID = "a4f01d8c-a037-43b6-9050-1876a8c23584";

// Characteristic
let txCharacteristic;
let rxCharacteristic;

let searchButton;
let readButton;

let loading;

function init() {
  searchButton = document.querySelector("#search-button");
  searchButton.addEventListener("click", searchBLE);
}

// search & connect
function searchBLE() {

  // acceptAllDevicesの場合optionalServicesが必要みたい
  navigator.bluetooth.requestDevice({
    optionalServices:[SERVICE_UUID],
    acceptAllDevices:true
  })
  
    .then(device => { 
      console.log("devicename:" + device.name);
      console.log("id:" + device.id);

      // 選択したデバイスに接続
      return device.gatt.connect();
    })

    .then(server => {
      console.log("success:connect to device");

      // UUIDに合致するサービス(機能)を取得
      return server.getPrimaryService(SERVICE_UUID);
    })

    .then(service => {
      console.log("success:service");
      // UUIDに合致するキャラクタリスティック(サービスが扱うデータ)を取得
      // 配列で複数のキャラクタリスティックの取得が可能
      return Promise.all([
        service.getCharacteristic(RX_CHARACTERISTIC_UUID),
        service.getCharacteristic(TX_CHARACTERISTIC_UUID)
      ]);
      
    })
    .then(characteristic => {
      console.log("success:txcharacteristic");

      rxCharacteristic = characteristic[0];
      txCharacteristic = characteristic[1];

      console.log("success:connect BLE");      
      loading.className = "hide";
    })

    .catch(error => {
      console.log("Error : " + error);

      // loading非表示
      loading.className = "hide";
    });
}

//グローバル変数に変更した
let message;

function readValueBLE() {
  let message;

  try {
    rxCharacteristic.readValue()
      .then(value => {
        message = value.buffer;
        console.log(new Uint8Array(message));
        var value　=　new TextDecoder("utf-8").decode(message)
        var num = parseInt(value, 10);
        console.log(num);
        document.getElementById("data-form").value = new TextDecoder("utf-8").decode(message);
      });
  }
  catch (e) {
    console.log(e);
  }
}

function writeValueBLE() {
  var form_d = document.getElementById("data-form").value;
  var ary_u8 = new Uint8Array( new TextEncoder("utf-8").encode(form_d) );
  console.log(ary_u8);
  try {
    txCharacteristic.writeValue(ary_u8);
  }
  catch (e) {
    console.log(e);
  }
}

window.addEventListener("load", init);

window.onload = function(){
  var dps = []; //dataPoints
  var chart = new CanvasJS.Chart("chartContainer", {
    width: 450,
    height: 350,
    axisY:{
          includeZero: true
    },
    data:[{
      type: "line",
      dataPoints: dps,
    }]
  });


  var xVal = 0;
  var yVal = 255;
  //下記は2000[ms](2秒)ごとにグラフを更新するように変数を指定。
  //ESP32からも2秒ごとにデータを送信するようにしているので、intervalの時間を合わせておくとわかりやすい。
  var updateInterval = 2000; 
  var dataLength = 20;
  var updateChart = function(count){

    try {
      rxCharacteristic.readValue()
        .then(value => {
          message = value.buffer;
          console.log(new Uint8Array(message));
          document.getElementById("data-form").value = new TextDecoder("utf-8").decode(message);
        });
    }
    catch (e) {
      console.log(e);
    }

    count = count || 1;
 
    for (var j = 0; j < count; j++) {
    
      //ESP32からBLEで送られてきたデータをInt型に変換し、変数にnumに代入//
      var value　=　new TextDecoder("utf-8").decode(message)
      var num = parseInt(value, 10);
      console.log(num);
      ///////////////////////////////////////////////////////////

      yVal = num;
      dps.push({
        x: xVal*2, //intervalが2秒なのでこうした
        y: yVal
      });
      xVal++;
    }
   
    if (dps.length > dataLength) {
      dps.shift();
    }

    chart.render();
  }

  updateChart(dataLength);
  setInterval(function(){updateChart()}, updateInterval);
  
}

///////////////////////////////////////////////////////////
var chartColors = {
	red: 'rgb(255, 99, 132)',
	orange: 'rgb(255, 159, 64)',
	yellow: 'rgb(255, 205, 86)',
	green: 'rgb(75, 192, 192)',
	blue: 'rgb(54, 162, 235)',
	purple: 'rgb(153, 102, 255)',
	grey: 'rgb(201, 203, 207)'
};

function randomScalingFactor() {
	return (Math.random() > 0.5 ? 1.0 : -1.0) * Math.round(Math.random() * 100);
}

function onRefresh(chart) {

  try {
    rxCharacteristic.readValue()
      .then(value => {
        message = value.buffer;
        console.log(new Uint8Array(message));
        document.getElementById("data-form").value = new TextDecoder("utf-8").decode(message);
      });
  }
  catch (e) {
    console.log(e);
  }

  var value　=　new TextDecoder("utf-8").decode(message)
  var num = parseInt(value, 10);
  console.log(num);

	chart.config.data.datasets.forEach(function(dataset) {
		dataset.data.push({
      x: Date.now(),
      y: num //y軸の値はESP32から送られてくるデータ
		});
	});
}

var color = Chart.helpers.color;
var config = {
	type: 'line',
	data: {
		datasets: [{
			label: 'Dataset 1 (linear interpolation)',
			backgroundColor: color(chartColors.red).alpha(0.5).rgbString(),
			borderColor: chartColors.red,
			fill: false,
			lineTension: 0,
			borderDash: [8, 4],
			data: []
		}]
	},
	options: {
		title: {
			display: true,
			text: 'Line chart (hotizontal scroll) sample'
		},
		scales: {
			xAxes: [{
				type: 'realtime',
				realtime: {
					duration: 20000,
					refresh: 2000,
					delay: 2000,
					onRefresh: onRefresh
				}
			}],
			yAxes: [{
				scaleLabel: {
					display: true,
					labelString: 'value'
				}
			}]
		},
		tooltips: {
			mode: 'nearest',
			intersect: false
		},
		hover: {
			mode: 'nearest',
			intersect: false
		}
	}
};

window.onload = function() {
	var ctx = document.getElementById('myChart').getContext('2d');
	window.myChart = new Chart(ctx, config);
};

```

### CSS
CSSは省略

# 実際の動き

![a3d037d662da89ec9b101c4e0e21a189.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212276/ccd2cb3b-4c9a-2094-3cee-304bf4853c85.gif)

index.htmlを起動し、SEARCHボタンを押して、電源に繋げてあるESP32とペアリングします。あとは、ESP32から0〜255までのランダム数が送られ、グラフがリアルタイムで描画されていきます。

# おわりに
ESP32のBLEやWEB Bluetoothいい感じに使えそうです。