M5AtomS3RからBLEでChromeに情報を送ってみた
AC電源の電圧と周波数を計測する基板を製作し、M5AtomS3R から BLE を用いてPC側の Chrome にセンサデータを送ってみました。
Web Bluetooth API の制限
今回は電圧と周波数の情報のみを M5AtomS3R で送るため、本来ならばBLEデバイスからの送信処理はアドバタイジングのみで十分です。このアドバタイジングデータをスキャンすることで他のBLEからデータを受信する方法については以前、SwitchBot防水温湿度計のデータを読む ことで既に実現できていました。
しかし現状の Web Bluetooth API、つまりPC側の Chrome ではアドバタイジングデータを直接的にスキャンする機能は提供されていません。また、接続を前提としたデバイス探索が必要であり、サービスUUIDを指定せず任意のBLEデバイスに接続して情報を読み取ることは許可されていません。
今回はアドバタイジングデータを送りながらも、Chrome 側からの接続処理に対応した送信方法を実装しました。
M5AtomS3R側プログラム
まず最初に送信側のプログラムを載せます。
main.cpp / platformio.ini (フルコード)
#include <Arduino.h>
#include <BLE2902.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <M5AtomS3.h>
#include <cstring>
#include "HLW8032.h"
#include "freqcount.h"
#define BLE_SERVICE_UUID "ac56855b-2669-46ec-93fe-01b47b2e1ebb"
#define BLE_CHARACTERISTIC_UUID "5e7566ad-e7da-4d94-8c93-5fac245e809a"
#define PIN_IN_FREQ 5
#define PIN_IN_SD 39
#define PIN_IN_PF 38
M5GFX lcd;
M5Canvas canvas(&M5.Lcd);
FreqCountIRQ<PIN_IN_FREQ> freq_count;
HLW8032 sensor;
BLEServer *pServer = nullptr;
BLECharacteristic *pCharacteristic = nullptr;
BLEAdvertising *pAdvertising = nullptr;
bool deviceConnected = false;
class BLECallbacks : public BLEServerCallbacks {
void onConnect(BLEServer *pBLEServer) override {
deviceConnected = true;
Serial.println("Device connected");
};
void onDisconnect(BLEServer *pBLEServer) override {
deviceConnected = false;
Serial.println("Device disconnected");
}
};
void updateDisplay() {
char buff[20];
static uint8_t seq;
bool freqIsValid = freq_count.update();
float freq = (float)(freq_count.get_observed_frequency() * 0.9999724008 + 0.0000109997);
float voltage = sensor.getEffectiveVoltage();
canvas.clear(BLACK);
canvas.setTextFont(4);
canvas.setTextColor(GREENYELLOW, BLACK);
dtostrf(voltage, 3, 3, buff);
canvas.drawRightString(buff, 100, 0);
canvas.drawString("V", 100, 0);
if (freqIsValid) {
canvas.setTextColor(ORANGE, BLACK);
dtostrf(freq, 2, 3, buff);
canvas.drawRightString(buff, 100, 20);
canvas.drawString("Hz", 100, 20);
} else {
canvas.setTextColor(ORANGE, BLACK);
canvas.drawRightString("--.---", 100, 20);
canvas.drawString("Hz", 100, 20);
}
BLEAdvertisementData advertisementData = BLEAdvertisementData();
advertisementData.setFlags(0x06); // BR_EDR_NOT_SUPPORTED | General Discoverable Mode
std::string strServiceData = "";
strServiceData += (char)0x0c; // 長さ(12Byte)
strServiceData += (char)0x16; // Type 0x16: Service Data
strServiceData += (char)0x85; // Service Data UUID
strServiceData += (char)0x5b; // Service Data UUID
strServiceData += (char)seq++;
strServiceData.append(reinterpret_cast<const char *>(&voltage), sizeof(float));
strServiceData.append(reinterpret_cast<const char *>(&freq), sizeof(float));
advertisementData.addData(strServiceData);
pAdvertising->setAdvertisementData(advertisementData);
pCharacteristic->setValue(strServiceData.c_str());
pCharacteristic->notify();
canvas.setTextFont(2);
if (deviceConnected) {
canvas.setTextColor(ORANGE, BLACK);
canvas.drawString("connected", 0, 40);
} else {
canvas.setTextColor(ORANGE, BLACK);
canvas.drawString("disconnected", 0, 40);
}
canvas.pushSprite(0, 0);
}
void setup() {
auto config = M5.config();
AtomS3.begin(config);
Serial.begin(115200);
M5.Lcd.init();
BLEDevice::init("ACObservator");
pServer = BLEDevice::createServer();
pServer->setCallbacks(new BLECallbacks());
BLEService *pService = pServer->createService(BLE_SERVICE_UUID);
pCharacteristic = pService->createCharacteristic(BLE_CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_NOTIFY);
pCharacteristic->addDescriptor(new BLE2902());
pService->start();
pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(BLE_SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->setMinPreferred(0x06);
pAdvertising->setMinPreferred(0x12);
canvas.setColorDepth(8);
canvas.createSprite(M5.Lcd.width(), M5.Lcd.height());
pinMode(PIN_IN_FREQ, INPUT);
pinMode(PIN_IN_SD, INPUT);
pinMode(PIN_IN_PF, INPUT);
Serial1.begin(4800, SERIAL_8E1, PIN_IN_SD);
freq_count.begin();
canvas.setTextSize(1);
}
void loop() {
AtomS3.update();
while (Serial1.available() > 0) {
sensor.processData(Serial1.read());
}
updateDisplay();
pAdvertising->start();
delay(1000);
pAdvertising->stop();
}
[platformio]
default_envs = m5stack-atoms3r-m5unified
[env:m5stack-atoms3r-m5unified]
extends = m5stack-atoms3r-m5unified
platform = espressif32
board = m5stack-atoms3
framework = arduino
build_flags =
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1
-DCORE_DEBUG_LEVEL=3
-DCONFIG_IDF_TARGET_ESP32S3
-D_ATOMS3R
-DARDUINO_ESP32S3_DEV
-DBOARD_HAS_PSRAM
-mfix-esp32-psram-cache-issue
lib_deps =
m5stack/M5GFX@^0.1.17
m5stack/M5Unified@^0.1.17
m5stack/M5AtomS3@^1.0.1
fastled/FastLED@^3.9.2
https://github.com/nanase/HLW8032.git
https://github.com/nanase/freqcount.git
解説
今回はAC電源の電圧と周波数のデータを1秒ごとにBLEで送信しています。その計測データの取得と計算は HLW8032 と freqcount という自作のライブラリを使っています。データの取得と表示部分を抜粋すると以下のようになります。
#include "HLW8032.h"
#include "freqcount.h"
void updateDisplay() {
// 取得処理
float freq = (float)(freq_count.get_observed_frequency() * 0.9999724008 + 0.0000109997);
float voltage = sensor.getEffectiveVoltage();
// LCDへの表示処理
...
}
void setup() {
pinMode(PIN_IN_FREQ, INPUT);
pinMode(PIN_IN_SD, INPUT);
Serial1.begin(4800, SERIAL_8E1, PIN_IN_SD);
freq_count.begin();
}
void loop() {
// センサーデータの読み取り
while (Serial1.available() > 0) {
sensor.processData(Serial1.read());
}
updateDisplay();
}
センサーデータの取得とLCDへの表示部の処理(フルコード)
#include <Arduino.h>
#include <M5AtomS3.h>
#include "HLW8032.h"
#include "freqcount.h"
#define PIN_IN_FREQ 5
#define PIN_IN_SD 39
#define PIN_IN_PF 38
M5GFX lcd;
M5Canvas canvas(&M5.Lcd);
FreqCountIRQ<PIN_IN_FREQ> freq_count;
HLW8032 sensor;
void updateDisplay() {
char buff[20];
bool freqIsValid = freq_count.update();
float freq = (float)(freq_count.get_observed_frequency() * 0.9999724008 + 0.0000109997);
float voltage = sensor.getEffectiveVoltage();
canvas.clear(BLACK);
canvas.setTextFont(4);
canvas.setTextColor(GREENYELLOW, BLACK);
dtostrf(voltage, 3, 3, buff);
canvas.drawRightString(buff, 100, 0);
canvas.drawString("V", 100, 0);
if (freqIsValid) {
canvas.setTextColor(ORANGE, BLACK);
dtostrf(freq, 2, 3, buff);
canvas.drawRightString(buff, 100, 20);
canvas.drawString("Hz", 100, 20);
} else {
canvas.setTextColor(ORANGE, BLACK);
canvas.drawRightString("--.---", 100, 20);
canvas.drawString("Hz", 100, 20);
}
canvas.pushSprite(0, 0);
}
void setup() {
auto config = M5.config();
AtomS3.begin(config);
Serial.begin(115200);
M5.Lcd.init();
canvas.setColorDepth(8);
canvas.createSprite(M5.Lcd.width(), M5.Lcd.height());
pinMode(PIN_IN_FREQ, INPUT);
pinMode(PIN_IN_SD, INPUT);
pinMode(PIN_IN_PF, INPUT);
Serial1.begin(4800, SERIAL_8E1, PIN_IN_SD);
freq_count.begin();
canvas.setTextSize(1);
}
void loop() {
AtomS3.update();
while (Serial1.available() > 0) {
sensor.processData(Serial1.read());
}
updateDisplay();
}
取得したセンサデータの送信部分の処理は以下のようになります。
Chrome ではアドバタイジングのスキャンに制限があり、設定変更なしでBLEデバイスから情報を読み取るにはBLEデバイス側も接続処理が必要になります。そのため Characteristic(特性) の情報やディスクリプタの登録も行っています。
なお、今回はアドバタイジングで送るサービスデータと、接続処理後に送る Characteristic の内容は同一になっています。
#include <BLE2902.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <cstring>
#define BLE_SERVICE_UUID "ac56855b-2669-46ec-93fe-01b47b2e1ebb"
#define BLE_CHARACTERISTIC_UUID "5e7566ad-e7da-4d94-8c93-5fac245e809a"
BLEServer *pServer = nullptr;
BLECharacteristic *pCharacteristic = nullptr;
BLEAdvertising *pAdvertising = nullptr;
bool deviceConnected = false;
class BLECallbacks : public BLEServerCallbacks {
void onConnect(BLEServer *pBLEServer) override {
deviceConnected = true;
Serial.println("Device connected");
};
void onDisconnect(BLEServer *pBLEServer) override {
deviceConnected = false;
Serial.println("Device disconnected");
}
};
void updateDisplay() {
BLEAdvertisementData advertisementData = BLEAdvertisementData();
advertisementData.setFlags(0x06); // BR_EDR_NOT_SUPPORTED | General Discoverable Mode
std::string strServiceData = "";
strServiceData += (char)0x0c; // 長さ(12Byte)
strServiceData += (char)0x16; // Type 0x16: Service Data
strServiceData += (char)0x85; // Service Data UUID
strServiceData += (char)0x5b; // Service Data UUID
strServiceData += (char)seq++;
strServiceData.append(reinterpret_cast<const char *>(&voltage), sizeof(float));
strServiceData.append(reinterpret_cast<const char *>(&freq), sizeof(float));
advertisementData.addData(strServiceData);
pAdvertising->setAdvertisementData(advertisementData);
pCharacteristic->setValue(strServiceData.c_str());
pCharacteristic->notify();
if (deviceConnected) {
canvas.setTextColor(ORANGE, BLACK);
canvas.drawString("connected", 0, 40);
} else {
canvas.setTextColor(ORANGE, BLACK);
canvas.drawString("disconnected", 0, 40);
}
...
}
void setup() {
BLEDevice::init("ACObservator");
pServer = BLEDevice::createServer();
pServer->setCallbacks(new BLECallbacks());
BLEService *pService = pServer->createService(BLE_SERVICE_UUID);
pCharacteristic = pService->createCharacteristic(BLE_CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_NOTIFY);
pCharacteristic->addDescriptor(new BLE2902());
pService->start();
pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(BLE_SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->setMinPreferred(0x06);
pAdvertising->setMinPreferred(0x12);
}
void loop() {
updateDisplay();
pAdvertising->start();
delay(1000);
pAdvertising->stop();
}
おまけ: BLEで1秒ごとにシーケンス番号を送るだけのコード
#include <Arduino.h>
#include <BLE2902.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <M5AtomS3.h>
#include <cstring>
#define BLE_SERVICE_UUID "ac56855b-2669-46ec-93fe-01b47b2e1ebb"
#define BLE_CHARACTERISTIC_UUID "5e7566ad-e7da-4d94-8c93-5fac245e809a"
BLEServer *pServer = nullptr;
BLECharacteristic *pCharacteristic = nullptr;
BLEAdvertising *pAdvertising = nullptr;
void setup() {
auto config = M5.config();
AtomS3.begin(config);
BLEDevice::init("BLETestDevice");
pServer = BLEDevice::createServer();
BLEService *pService = pServer->createService(BLE_SERVICE_UUID);
pCharacteristic = pService->createCharacteristic(BLE_CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_NOTIFY);
pCharacteristic->addDescriptor(new BLE2902());
pService->start();
pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(BLE_SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->setMinPreferred(0x06);
pAdvertising->setMinPreferred(0x12);
}
void loop() {
static uint8_t seq;
BLEAdvertisementData advertisementData = BLEAdvertisementData();
advertisementData.setFlags(0x06); // BR_EDR_NOT_SUPPORTED | General Discoverable Mode
std::string strServiceData = "";
strServiceData += (char)0x04; // 長さ(4Byte)
strServiceData += (char)0x16; // Type 0x16: Service Data
strServiceData += (char)0x85; // Service Data UUID
strServiceData += (char)0x5b; // Service Data UUID
strServiceData += (char)seq++;
advertisementData.addData(strServiceData);
pAdvertising->setAdvertisementData(advertisementData);
pCharacteristic->setValue(strServiceData.c_str());
pCharacteristic->notify();
pAdvertising->start();
delay(1000);
pAdvertising->stop();
}
おまけ: BLEのアドバタイジングのみで1秒ごとにシーケンス番号を送るだけのコード
#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <M5AtomS3.h>
#include <cstring>
#define BLE_SERVICE_UUID "ac56855b-2669-46ec-93fe-01b47b2e1ebb"
BLEServer *pServer = nullptr;
BLEAdvertising *pAdvertising = nullptr;
void setup() {
auto config = M5.config();
AtomS3.begin(config);
BLEDevice::init("BLETestDevice");
pServer = BLEDevice::createServer();
pService->start();
pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(BLE_SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->setMinPreferred(0x06);
pAdvertising->setMinPreferred(0x12);
}
void loop() {
static uint8_t seq;
BLEAdvertisementData advertisementData = BLEAdvertisementData();
advertisementData.setFlags(0x06); // BR_EDR_NOT_SUPPORTED | General Discoverable Mode
std::string strServiceData = "";
strServiceData += (char)0x04; // 長さ(4Byte)
strServiceData += (char)0x16; // Type 0x16: Service Data
strServiceData += (char)0x85; // Service Data UUID
strServiceData += (char)0x5b; // Service Data UUID
strServiceData += (char)seq++;
advertisementData.addData(strServiceData);
pAdvertising->setAdvertisementData(advertisementData);
pAdvertising->start();
delay(1000);
pAdvertising->stop();
}
このプログラムを M5AtomS3R に書き込むとすぐさまBLEによるアドバタイジングデータの送信が始まります。Androidアプリの nRF Connect でスキャンを行うと、ACObservator
というデバイスが発見でき、センサデータを含んだ Service Data も発見できます。
また、同アプリで接続を行うと Unknown Service と Unknown Characteristic を発見でき、その内部にアドバタイジングデータの Service Data と同一のデータが格納されているのを確認できます。
この Unknown Service と Unknown Characteristic が見れる状態であれば Chrome 側でも接続ができる状態になります。
Chrome側プログラム
今回は Vue と VueUse と Vuetify を使います。特に VueUse には useBluetooth
という便利なコンポーザブル関数があるのでこれを使います。
なお useBluetooth
にて filters
を指定しているのは、セキュリティ上の制約のためです。これも前述しましたが、現在の Chrome では接続可能なBLEデバイスを発見することはできますが、データを読み取るためには Service UUID によるフィルタ指定が必須となります。
<script setup lang="ts">
import { ref } from 'vue';
import { whenever, useBluetooth } from '@vueuse/core';
const deviceNamePrefix = 'ACObservator';
const serviceUUID = 'ac56855b-2669-46ec-93fe-01b47b2e1ebb';
const characteristicUUID = '5e7566ad-e7da-4d94-8c93-5fac245e809a';
const { isSupported, isConnected, device, requestDevice, server } = useBluetooth({
acceptAllDevices: true,
filters: [{ services: [serviceUUID] }, { namePrefix: deviceNamePrefix }],
});
const sequence = ref<number>(0);
const voltage = ref<number>(Number.NaN);
const frequency = ref<number>(Number.NaN);
whenever(
() => isConnected.value && server.value,
async (server) => {
const service = await server.getPrimaryService(serviceUUID);
const characteristic = await service.getCharacteristic(characteristicUUID);
await characteristic.startNotifications();
characteristic.addEventListener('characteristicvaluechanged', function () {
sequence.value = this.value?.getUint8(4) ?? Number.NaN;
voltage.value = this.value?.getFloat32(5, true) ?? Number.NaN;
frequency.value = this.value?.getFloat32(9, true) ?? Number.NaN;
});
},
);
</script>
あとは requestDevice
を呼び出すようなボタンを設置し、
<v-menu>
<template #activator="{ props }">
<v-btn icon v-bind="props">
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item title="デバイスとの接続" @click="requestDevice()" />
</v-list>
</v-menu>
簡易的ですが表示部分を作ります。
<p>isSupported: {{ isSupported }}</p>
<p>isConnected: {{ isConnected }}</p>
<p>device: {{ device }}</p>
<p>server: {{ server }}</p>
<p>sequence: {{ sequence }}</p>
<p>voltage: {{ voltage }}</p>
<p>frequency: {{ frequency }}</p>
接続ボタンを押すと、接続デバイスの候補ダイアログが出ます。フィルタ部分で Service UUID と Name Prefix でフィルタしているので、ここでは1つしか表示がないはずです。
デバイスを指定すると、1秒ごとに基板側のM5AtomS3Rから送られてきたデータが表示されます。
なお、センサデータの読み取り部分の getFloat32
関数の第2引数はエンディアンネスの指定になります。省略するとビッグエンディアンとして、true
の場合はリトルエンディアンとして float の値を読み取ります。
また、useBluetooth
を使うと Web Bluetooth API に関わる型が解決できないことがあります。その場合 @types/web-bluetooth
パッケージを導入し、
$ yarn add -D @types/web-bluetooth
tsconfig.json にて型定義ファイルの追加指定が必要になります。
{
"compilerOptions": {
"types": [
"web-bluetooth"
]
}
}
参考文献
-
M5StackでBLE環境センサー端末を作る
https://ambidata.io/samples/m5stack/m5stack_ble_sensor/ -
Ⅰ. ESP32によるBLEアプリケーション開発の基礎知識
http://marchan.e5.valueserver.jp/cabin/comp/jbox/arc212/doc21201.html -
ESP32を使ったBLE実装時の再接続について
https://qiita.com/IRumA/items/00fc746892570f8d1c38 -
Advertising Data - BLEDocs
https://fabo.gitbooks.io/bledocs/content/nordic/beaconadvdata.html
Discussion