はじめに
ESP32-DevkitCが手元にあるので、ESP32のBLEとWeb bluetoothを使って、Web上にデータをリアルタイム表示させてみました。
マイコン(ESP32)からのデータ送信は下記を参考にしました。
-
[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
- リアルタイムストリーミングデータ向けのchart.jsのプラグインで、ストリーミングチャートを簡単に生成できる
- リンク先のサンプルプログラムをがっつり参考にした
- moment.js
- 日付をいい感じに使えるようにしてくれjs
プログラム
Esp32
#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
<!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
// 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は省略
実際の動き
index.htmlを起動し、SEARCHボタンを押して、電源に繋げてあるESP32とペアリングします。あとは、ESP32から0〜255までのランダム数が送られ、グラフがリアルタイムで描画されていきます。
おわりに
ESP32のBLEやWEB Bluetoothいい感じに使えそうです。