M5stack core2とM5Atomをbluetoothで通信した記事を以前書きました。
bluetoothについて詳しくなかったため、ライブラリが混在したプログラムになっていました。bluetoothについての知識を整理したうえで、作成したプログラムを説明していきます。今回は、BLEを用いて通信しています。
1. Bluetooth ClassicとBluetooth Low Energy(BLE)
1-1. 比較
Bluetooth Classicは、1999年に一般公開されました。そののち、Bluetooth Low Energy(省電力に特化した機能以降BLE)が2009年に追加されました。以降BLEに対して様々な機能が追加されています。比較してみるとBLEの方が消費電力を抑え、接続範囲も広く、接続数も多いことが分かります。
1-1-1. Bluetooth Classic
Bluetooth Classicについて、スマホとヘッドフォンを例にして説明します。
スマホ側は、マスターまたはClient、ヘッドフォン側は、スレーブまたは、Serverとなり、ピコネットを形成します。ピコネットとは、bluetooth接続によって作られている小さなネットワークです。
スレーブ側は、Serverとして接続できるようにするためにアドバタイズします。アドバタイズとは、自分が受信できることを周知します。スマホを開いてbluetoothの接続画面で見ることができる接続先は、このアドバタイズが行われいることを示しています。Client側では、スキャン、受信、アドバタイズパケットしている結果を見ています。アドバタイズパケットとは、Serverとして周囲に示すために定期的に発信している信号のことです。一度接続が行われると、ペアリングとなり、自動で再接続されます。ただ、別のClientをする場合、Server端末は、別のピコネットにいるため、手動でペアリングを切り変える必要があります。
1-1-2. SerialBT
Bluetooth Classicは、SerialBTを用いて接続します。マスター側は、スレーブ側のMACアドレスを用いて通信を行います。詳細については、時間があるときに記事にしていきます。
1-2-1. BLE
BLEについてです。セントラル側は、ベリフェラル側にリード、ライトの要求を出したり、ベリフェラル側で何かボタンを押したときの結果をセントラル側に送ることができます。ベリフェラル側は、サービスとキャラクタリスティックに実行する内容を記述します。サービスは、キャラクタリスティックによって構成されるクラスとして機能します。サービスの中にどう実行するかをまとめているのものが、キャラクタリスティックのようです。この値をセントラル側がアクセスすることで動作します。
接続は、ベリフェラル側がアドバタイズ(広告する、周りに自分がいることを知らせる)します。接続する際の名前を自由につけることができます。セントラル側でスキャンを実行し、ベリフェラル側の名前を見つけて接続します。セントラル側は、ベリフェラル側へリクエストを送ることができます。ベリフェラル側に値を書き込むwrite、ベリフェラルのデータを読み込むread、ベリフェラルの操作によってセントラル側にデータを送るNorifyという操作があります。
1-2-3. 接続方式
接続方式は、2つにわけることができます。ブロードキャスト型は、あるデバイスがアドバタイズをして周囲に自分の存在や情報を伝えるために利用します。接続型は、通信する相手を特定し双方向で通信を行うことができます。
2. M5stack core2とM5Atomの通信
M5stack core2とM5atomを通信します。M5atomには、土壌センサをつけています。今回、M5stack core2は、セントラル、M5atomは、ベリフェラルとしています。通信はNotifyとして、M5atomでボタンを押したときに、M5stack core2にデータを転送させます。
2-1. 環境
OS: windows 10.0
Arduino IDE:2.0.0
デバイス:M5stackcore2,M5atom,M5Stack用土壌水分センサユニット
2-2. コーディング
プログラムは、githubに置いています。プログラムは、こちらに置いています。
2-2-1. M5atom(ベリフェラル)
M5atomで行われるbluetoothの動作を簡単に説明します。BLEDevice::init(SERVER_NAME);
でサーバーとしてスキャンされたときに表示される名前になります。BLEService *pService = pServer->createService(SERVICE_UUID)
では、サービスのUUIDを設定しています。pCharacteristic = pService->createCharacteristic
は、ベリフェラルとしてread、write、Notifyとして使用できるようにします。
pService->start()
でサーバとして検索できるようにしています。
UUIDは、Online UUID Generatorから取得して適当な値を入れています。
ボタンが押されたとき、pCharacteristic->setValue(analogRead_value);
でセンサから取得した値をセットし、pCharacteristic->notify();
で、norifyとしてデータをセントラル側に送るようにしています。
#include "M5Atom.h"
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <BLE2902.h>
#define SERVICE_UUID "8cf0a2f0-164d-4096-adef-9e89bc20044e"
#define CHARACTERISTIC_UUID "b5d67235-f2f0-4f81-9448-e029f3f4ea89"
#define SERVER_NAME "M5Atom-earth"
BLEServer* pServer = NULL;
BLECharacteristic* pCharacteristic = NULL;
bool deviceConnected = false;
uint16_t analogRead_value = 0;
uint16_t digitalRead_value = 0;
uint16_t device_id = 0x0101; // your any sensor device id
void readEarthSensor() {
analogRead_value = analogRead(32);
digitalRead_value = digitalRead(26);
Serial.printf("0x%04x, %d, %d\n", device_id, digitalRead_value, analogRead_value);
}
void print_readEarthSensor() {
analogRead_value = analogRead(32);
digitalRead_value = digitalRead(26);
pCharacteristic->setValue(analogRead_value);
pCharacteristic->notify();
}
class MyServerCallbacks: public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
Serial.println("connect");
deviceConnected = true;
};
void onDisconnect(BLEServer* pServer) {
Serial.println("disconnect");
deviceConnected = false;
}
};
class MyCallbacks: public BLECharacteristicCallbacks {
void onRead(BLECharacteristic *pCharacteristic) {
Serial.println("read");
// std::string value = pCharacteristic->getValue();
// Serial.println(value.c_str());
}
void onWrite(BLECharacteristic *pCharacteristic) {
Serial.println("write");
}
};
void setup() {
M5.begin(true, false, true);
delay(10);
Serial.begin(115200);
M5.dis.clear();
pinMode(26, INPUT);
Serial.println("\nM5Atom Earth Test");
// Read earth sensor value
readEarthSensor();
BLEDevice::init(SERVER_NAME);
BLEServer *pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
BLEService *pService = pServer->createService(SERVICE_UUID);
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE |
BLECharacteristic::PROPERTY_NOTIFY
);
pCharacteristic->setCallbacks(new MyCallbacks());
pCharacteristic->addDescriptor(new BLE2902());
pService->start();
BLEAdvertising *pAdvertising = pServer->getAdvertising();
pAdvertising->start();
}
void loop() {
// if (deviceConnected) {
if (M5.Btn.wasPressed()) {
// Read earth sensor value
readEarthSensor();
print_readEarthSensor();
}
// }
M5.update();
delay(1*1000);
}
2-2-2. M5stack core2(セントラル)
M5stack core2は、セントラルとして使用しています。接続したいサーバー名とベリフェラル側で設定したUUIDを使用し、所望のデバイスを識別し、通信を行います。セントラル側では、周囲にあるサーバーをスキャンして一致するものを見つけたら、そのデバイスにアクセスして通信しています。sketch_feb25a.ino
のM5.BtnC.wasPressed()``でbluetoothで通信した値を出力できるようにプログラムを作成しました
define.ino```は、コードを分かりやすくするために使用した関数をまとめています。
#include <M5Core2.h>
#include <WiFi.h>
#include <time.h>
#include <M5_ENV.h>
#include <stdio.h>
#include <stdlib.h>
#include <BLEDevice.h>
#include <string>
// 構造体の宣言
typedef struct {
char *str;
} strct;
//Bluetooth
#define SERVER_NAME "M5Atom-earth"
static BLEUUID serviceUUID("8cf0a2f0-164d-4096-adef-9e89bc20044e");
static BLEUUID charUUID("b5d67235-f2f0-4f81-9448-e029f3f4ea89");
static BLEAddress *pServerAddress;
static boolean doConnect = false;
static boolean connected = false;
static BLERemoteCharacteristic* pRemoteCharacteristic;
uint16_t earth_analog_val = 0;
// sprite
TFT_eSprite sprite = TFT_eSprite(&M5.Lcd);
// csv file
//const char* fname = "/log.csv";
File file;
// set up sensor
SHT3X sht30;
QMP6988 qmp6988;
long last_millis = 0;
// for WiFi
char ssid[] = "please input your SSID"; // your SSID
char pass[] = "please input your Pass word"; // your Pass word
float tmp = 0.0;
float hum = 0.0;
float pressure = 0.0;
int earth_degtal = 0;
int earth_analog = 0;
int display_btm = 1;
int con_count = 0;
int timer_reset = 0;
int bule_address = 0;
bool meas_csv = false;
bool write_csv = false;
char info[40];
// for Time
const char* ntpServer = "ntp.jst.mfeed.ad.jp"; // NTP server
const long gmtOffset_sec = 9 * 3600; // Time offset 9hr
const int daylightOffset_sec = 0; // No summer time
RTC_DateTypeDef RTC_DateStruct; // Data
RTC_TimeTypeDef RTC_TimeStruct; // Time
struct tm timeinfo;
String dateStr;
String timeStr;
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
Serial.print("BLE Advertised Device found: ");
Serial.println(advertisedDevice.toString().c_str());//Address: 30:ae:a4:02:a3:fe, txPower: -21
Serial.println(advertisedDevice.getName().c_str());//M5Atom-earth
if(advertisedDevice.getName()==SERVER_NAME){
Serial.println(advertisedDevice.getAddress().toString().c_str());
advertisedDevice.getScan()->stop();
pServerAddress = new BLEAddress(advertisedDevice.getAddress());
doConnect = true;
}
}
};
void setup() {
M5.begin();
M5.Lcd.setBrightness(10);
M5.Lcd.setRotation(1);
M5.Lcd.setTextSize(2);
M5.Lcd.fillScreen(BLACK);
Serial.println("Starting Arduino BLE Client application...");
BLEDevice::init("");
BLEScan* pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
pBLEScan->setInterval(1349);
pBLEScan->setActiveScan(true);
pBLEScan->start(5, false);
// connect wifi
if (timer_reset == 1){
WiFi.begin(ssid, pass);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
M5.Lcd.print(".");
con_count++;
if(con_count >= 20){
break;
}
}
if(con_count < 20){
M5.Lcd.print("\nWiFi connected.");
setNTP2RTC();
}else{
M5.Lcd.print("\nWiFi did not connect.");
}
M5.Lcd.print("\nIP=");
M5.Lcd.print(WiFi.localIP());
delay(2 * 1000);
}
// Output on the display.
M5.Lcd.fillScreen(BLACK);
M5.update();// update button state
delay(2 * 1000);
Wire.begin(); // // Wire init, adding the I2C bus.
qmp6988.init();
sprite.setColorDepth(8);
sprite.setTextSize(2);
sprite.fillScreen(BLACK);
sprite.createSprite(M5.lcd.width(), M5.lcd.height());
//make a directory
if(!SD.exists("/sensor_data")){
SD.mkdir("/sensor_data");
}
}
void loop() {
sprite.pushSprite( 0, 0);
M5.update();// update button state
//display select
if (M5.BtnA.wasPressed() == 1) display_btm = 1;
if (M5.BtnB.wasPressed() == 1) display_btm = 2;
if (M5.BtnC.wasPressed() == 1) display_btm = 3;
if(display_btm == 1){
// Get time
M5.Rtc.GetDate(&RTC_DateStruct);
M5.Rtc.GetTime(&RTC_TimeStruct);
pressure = qmp6988.calcPressure()* 0.01;
if (sht30.get() == 0) { // Obtain the data of shT30.
tmp = sht30.cTemp; // Store the temperature obtained from shT30.
hum = sht30.humidity; // Store the humidity obtained from the SHT30.
} else {
tmp = 0, hum = 0;
}
// for Touch
TouchPoint_t pos= M5.Touch.getPressPoint();
if(pos.y > 240){
if(pos.x < 109)
sprite.setTextColor(RED,BLACK);
else if(pos.x > 218)
sprite.setTextColor(BLUE,BLACK);
else if(pos.x >= 109 && pos.x <= 218)
sprite.setTextColor(GREEN,BLACK);
}
sprite.fillScreen(BLACK);
sprite.setTextColor(WHITE);
sprite.setCursor(0, 0, 1);
sprite.printf("%04d.%02d.%02d ", RTC_DateStruct.Year, RTC_DateStruct.Month, RTC_DateStruct.Date);
sprite.printf("%02d:%02d\n", RTC_TimeStruct.Hours, RTC_TimeStruct.Minutes);
sprite.printf("temperature: %4.1fdegC\n", tmp);
sprite.printf("humidity: %4.1f%%\n", hum);
sprite.printf("pressure: %4.1fhPa\n", pressure);
delay(1 * 1000);
}
if(display_btm == 2){
pressure = qmp6988.calcPressure()* 0.01;
if (sht30.get() == 0) { // Obtain the data of shT30.
tmp = sht30.cTemp; // Store the temperature obtained from shT30.
hum = sht30.humidity; // Store the humidity obtained from the SHT30.
} else {
tmp = 0, hum = 0;
}
sprite.fillScreen(BLACK);
printData_btm2(sprite, 10, 10, 4, ORANGE, "temperature", tmp, "deg C");
printData_btm2(sprite, 10, 80, 4, BLUE, "humidity", hum, "%");
printData_btm2(sprite, 10, 160, 4, TFT_MAROON,"pressure", pressure,"hPa");
delay(0.5 * 1000);
}
if(display_btm == 3){
if (doConnect == true) {
delay(1 * 1000);
if (connectToServer(*pServerAddress)) {
Serial.println("connected!");
connected = true;
} else {
Serial.println("We have failed to connect to the server.");
connected = false;
}
doConnect = false;
}
sprite.fillScreen(BLACK);
// print for earth
const char* bluetooth_status = connected ? "On" : "Off";
printData_btm3(sprite, 10, 10, 4, ORANGE, "DEVICE NAM", "EARTH");
printData_btm3(sprite, 10, 80, 4, BLUE, "Bluetooth connect", bluetooth_status);
printData_btm3_int(sprite, 10, 160, 4, TFT_MAROON, "Analog value", earth_analog_val);
delay(1 * 1000);
}
if (RTC_TimeStruct.Minutes % 5 == 4 ) meas_csv = true;
// get the csv per 5min
if(RTC_TimeStruct.Minutes % 5 == 0){
if (meas_csv == true){
//write data per 5min.
meas_csv = false;
write_csv = true ;
M5.Rtc.GetDate(&RTC_DateStruct);
M5.Rtc.GetTime(&RTC_TimeStruct);
pressure = qmp6988.calcPressure()* 0.01;
if (sht30.get() == 0) { // Obtain the data of shT30.
tmp = sht30.cTemp; // Store the temperature obtained from shT30.
hum = sht30.humidity; // Store the humidity obtained from the SHT30.
} else {
tmp = 0, hum = 0;
}
// for Touch
TouchPoint_t pos= M5.Touch.getPressPoint();
if(pos.y > 240){
if(pos.x < 109)
sprite.setTextColor(RED,BLACK);
else if(pos.x > 218)
sprite.setTextColor(BLUE,BLACK);
else if(pos.x >= 109 && pos.x <= 218)
sprite.setTextColor(GREEN,BLACK);
}
char fname[26];
snprintf(fname, 26, "/sensor_data/%04d%02d%02d.csv", RTC_DateStruct.Year, RTC_DateStruct.Month, RTC_DateStruct.Date);
// write data
if(!SD.exists(fname)){
file = SD.open(fname, FILE_APPEND);
file.print("date,time,temperature,humidity,pressure\n");
file.close();
delay(0.1 * 1000);
}
file = SD.open(fname, FILE_APPEND);
file.print(RTC_DateStruct.Month);
file.print("/");
file.print(RTC_DateStruct.Date);
file.print(",");
file.print(RTC_TimeStruct.Hours);
file.print(":");
if(RTC_TimeStruct.Minutes< 10) {
file.print("0");
}
file.print(RTC_TimeStruct.Minutes);
file.print(",");
file.print(tmp);
file.print(",");
file.print(hum);
file.print(",");
file.print(pressure);
file.print("\n");
file.close();
}
}
}
// ----------------------------------------------------------------------------
// --- RTC set up ----
// ----------------------------------------------------------------------------
void setNTP2RTC(){
// timeSet
getTimeFromNTP();
getLocalTime(&timeinfo);
// read RTC
M5.Rtc.GetTime(&RTC_TimeStruct);
M5.Rtc.GetDate(&RTC_DateStruct);
// --- to over write date&time
RTC_DateStruct.Year = timeinfo.tm_year + 1900;
RTC_DateStruct.Month = timeinfo.tm_mon + 1;
RTC_DateStruct.Date = timeinfo.tm_mday;
RTC_DateStruct.WeekDay = timeinfo.tm_wday;
M5.Rtc.SetDate(&RTC_DateStruct);
RTC_TimeStruct.Hours = timeinfo.tm_hour;
RTC_TimeStruct.Minutes = timeinfo.tm_min;
RTC_TimeStruct.Seconds = timeinfo.tm_sec;
M5.Rtc.SetTime(&RTC_TimeStruct);
}
void getTimeFromNTP(){
// To get Time from NTP server
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
while (!getLocalTime(&timeinfo)) {
delay(1000);
}
}
// ----------------------------------------------------------------------------
// --- BLE set up ----
// ----------------------------------------------------------------------------
static void notifyCallback(
BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) {
earth_analog_val=0;
earth_analog_val=(uint16_t)(pData[1]<<8 | pData[0]);
Serial.println(earth_analog_val);
}
bool connectToServer(BLEAddress pAddress) {
Serial.print("Forming a connection to ");
Serial.println(pAddress.toString().c_str());
BLEClient* pClient = BLEDevice::createClient();
pClient->connect(pAddress);
BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
//notify してクライアントの電源が切れると、サーバー側も電源を落とす。サービスが読み込めなくなるため。
Serial.println(pRemoteService->toString().c_str());
if (pRemoteService == nullptr) {
return false;
}
pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
if (pRemoteCharacteristic == nullptr) {
return false;
}
pRemoteCharacteristic->registerForNotify(notifyCallback);
return true;
}
// ----------------------------------------------------------------------------
// --- Botton operation ----
// ----------------------------------------------------------------------------
void printData_btm2(TFT_eSprite& sprite, int x, int y, int textSize, uint16_t color, const char* label, float value, const char* unit) {
sprite.setTextColor(WHITE);
sprite.setCursor(x, y, 1);
sprite.printf(label);
sprite.setTextColor(color);
sprite.setCursor(x + 70, y + 20, textSize);
sprite.printf("%4.1f", value);
sprite.setTextColor(color);
sprite.setCursor(x + 230, y + 30, textSize-1);
sprite.printf(unit);
}
void printData_btm3(TFT_eSprite& sprite, int x, int y, int textSize, uint16_t color, const char* label, const char* unit) {
sprite.setTextColor(WHITE);
sprite.setCursor(x, y, 1);
sprite.printf(label);
sprite.setTextColor(color);
sprite.setCursor(x + 50, y + 30, textSize);
sprite.printf(unit);
}
void printData_btm3_int(TFT_eSprite& sprite, int x, int y, int textSize, uint16_t color, const char* label,uint16_t value) {
sprite.setTextColor(WHITE);
sprite.setCursor(x, y, 1);
sprite.printf(label);
sprite.setTextColor(color);
sprite.setCursor(x + 50, y + 30, textSize);
sprite.printf("%d",value);
}
2-2-3. コードの改善点
今回作成したコードをデバックした際にフリーズしてしまうことが度々発生していました。調べてみると、一度接続をしたあとセントラル側をリセットしたときに、BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
でもう一度接続を試見たときにずっと探し続けてしまうことでフリーズしていました。セントラル側をリセットしたときは、ベリフェラル側森セットすれば、フリーズしなくなることが分かったので、いったんはその操作を行って接続できるようにしています。今後、プログラムを作成していくうえで問題となったときに改善予定です。
参考文献
作成したときに参考とした文献です。
iOS×BLE Core Bluetooth プログラミング
ESP32 BLE for Arduino
Getting Started with ESP32 Bluetooth Low Energy (BLE) on Arduino IDE
esp-nimble-cpp