M5StickCとラズパイ4とのBLE(Bluetooth Low Energy)通信を試してみた。
Contents
BLEでアドバタイジング
M5StickCにてBME280のセンサーモジュールで温湿度、気圧を測定してその測定結果をBLE(Bluetooth Low Energy)でアドバタイジング(ブロードキャスト)してラズパイ(RaspberryPi 4B)でスキャンして非同期でデータを受信するプログラムを作ってみた時の備忘録。
BLEとは
BLE(Bluetooth Low Energy)はBluetoothの拡張仕様のひとつでその名の通りLow Energy(省エネルギー)で無線通信が可能な規格。
最初の規格のBluetooth1.0では数mの通信距離だったので主にマウスやパソコンの周辺機器とのやりとりに使用されていたがBluetooth4.0では数十メートル、Bluetooth5.0ではデータレートを125kbpsと低速に設定すると約400メートルまで到達すると言われている。
また最新のBluetooth5.1には方向を探知する機能が追加された。
Bluetooth4.2 | データレート:1Mbps 到達距離:数十m~100m メッセージ容量:31Byte |
Bluetooth5.0 | データレート:2Mbps、1Mbps、500kbps、125kbps 到達距離:2、1Mbpsは100m、125kbpsは400m メッセージ容量:255Byte |
Bluetooth5.1 | ペアリング相手の方向を検知する機能がついた |
対応バージョン
それぞれの機器の対応しているBluetoothのバージョンは以下の通り。
Raspberry Pi Zero | Bluetooth 4.1 |
Raspberry Pi 3 Model B+ | Bluetooth4.2 |
Raspberry Pi 4 Model B | Bluetooth 5.0 |
M5StickC(ESP32-PICO-D4) | Bluetooth 4.2 |
今回はM5StickCとRaspberry Pi 4 Bとの通信なのでBluetooth4.2の規格でのBLE通信になると思われる。
速度はWi-Fiと比較すると低速だが省エネルギーなので電源が確保できない場所での温湿度の測定等で威力を発揮するのではないかと思い試してみることにした。
BLEはボタン電池で1年間稼働させる事が出来ると言われているほど省エネルギーなのでM5StickCに内蔵のリチウム電池ならプログラミングによってはかなりの長期間持たせることが出来るのではと思っている。
全体構成図
全体構成図は以下の通り。
- M5StickCのHY2.0-4P端子に温湿度・気圧測定センサー(BME280)を取り付ける
- センサーで取得した値をBLEで10秒間アドバタイジング(ブロードキャスト)する
- 数十メートル離れた場所でラズパイ4にてBLEで信号をスキャンしてコンソールに表示する
- ペアリングしての通信では無いので非同期でのデータ通信となる
必要な機器
用意した機器は以下の通り。
M5StickC
温湿度、気圧のセンサーを接続してBLEでアドバタイジングする為にM5StickCを用意した。
M5StickCに積んでいるチップ(ESP32-PICO-D4)はBluetoothの他にWi-Fiでの通信も可能だが電源を確保できない場所でので稼働も想定して省エネルギーのBluetoothでの通信としている。
M5StickC は下記の M5StickC Plus に置き換わっている。
スイッチサイエンスでは M5StickC(本体のみ)がまだ販売されていた。
温湿度、気圧センサー
温湿度、気圧を測定するためのセンサーモジュール(BME280)。
M5StickCからの電源供給は5.0Vなので電圧レギュレーターが入っていて3.3V、5.0Vの両方の使用が可能なタイプを選択した。
M5StickCのHY2.0-4P(Grove端子)に接続する。
尚、BME280の測定範囲や誤差等の詳細情報は以前の記事を参照して欲しい。
端子は、
- VIN(3.3、5.0Vの両方使用が可能)
- GND
- SCL
- SDA
の4つ。
M5StickCとはI2C(Inter-Integrated Circuit)通信で接続する。
Grove汎用ケーブル
M5StickCのHY2.0-4P(Grove端子)とBME280を接続する為のケーブル。
このケーブルは両端がGrove端子になっているのでBME280側は加工する必要がある。
ジャンパーワイヤーセット
上記のGrove汎用ケーブルは両端共にGrove端子になっているのでBME280側をジャンパーワイヤーセットの4ピンのメスに変更している。
Raspberry Pi
手元にあったRaspberry Pi 4Bを使用したがRaspberry Pi Zeroでもスペック的には問題ないと思われる。
使用した工具類
使用した工具類は以下の通り。
はんだごて
購入した温湿度、気圧センサーモジュール(BME280)にコネクタピンを固定するためにはんだごてを使用した。
はんだ
電子工作用の鉛入りのはんだであれば何でも良いと思う。
スタンドルーペ
必須では無いのだがはんだの時はあったほうが作業性は良くなる。
手元がLEDで照らされ、なおかつルーペで拡大されるので良く見えるのと基盤が固定されるのではんだ付けしやすい。
特に自分ははんだは得意では無いので失敗しないように道具でカバーしている。
電工ペンチ
Grove端子の片方を切断して4ピンのジャンパーワイヤーに付け替えるのに使用した。
電工ペンチは大型のものよりエーモンの小型の方が扱いやすいので多用している。
組み立て
BME280のはんだ付け
購入した温湿度、気圧センサーモジュールのBME280は下記の様にピンとモジュールが別々になっている。
ピンとモジュールをはんだ付けする。
コネクタの作成
購入したGrove汎用ケーブルは両端共にGrove端子だったので片方を切断して4ピンのジャンパーワイヤーに変更した。
ジャンパーワイヤーの加工については過去のこちらの記事を参照して欲しい。
配線図
M5StickCとBME280の配線図は以下の通り。
購入したGrove汎用ケーブルの黄色と白の色が逆転している(SDAを黄色、SCLを白にしたかった)がまぁ良しとする。
環境構築
M5StickC の開発環境である Arduino IDE を Windowsパソコンにインストールするのと Raspberry Pi 4B に事前に必要なライブラリやモジュールをインストールしておく。
Arduino IDE
開発環境であるWindows10マシンにArduino IDE(統合開発環境)をインストールした。
インストール手順の詳細については以前の「Arduino IDEで簡単なプログラム(C++)を作成してM5StickCで実行してみた」記事を参照してほしい。
続いてM5StickCの開発環境であるArduino IDEにAdafruit社のBME280のライブラリをインストールしておく。
Arduino IDEを起動してスケッチ、ライブラリをインクルード、ライブラリを管理を選択する。
検索欄で″BME280″で検索して現れるAdafruit BME280 Libraryを「インストール」する。
“Adafruit BME280ライブラリは複数の他のライブラリが必要”との事なので「install all」を選択した。
“INSTALLED”を確認したら「閉じる」ボタンで閉じる。
ラズパイ側
ラズパイ側にPythonからBluetoothデバイスを操作するためのモジュールbluepyを以下のコマンドでインストールしておく。
sudo pip3 install bluepy
スケッチ(プログラム)
M5StickC
温湿度、気圧を測定してアドバタイジング(ブロードキャスト)する側のM5StickCのプログラムは以下の通り。
スイッチサイエンス社のサイトを参考にさせて貰い、Arduino IDE(C++)でコーディングしている。
ソースコード
ble_pub.ino
#include <M5StickC.h>
#include <BLEDevice.h> // Bluetooth Low Energy
#include <BLEServer.h> // Bluetooth Low Energy
#include <BLEUtils.h> // Bluetooth Low Energy
#include <esp_sleep.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#define T_PERIOD 10 // アドバタイジングパケットを送る秒数
#define S_PERIOD 20 // Deep Sleepする秒数
RTC_DATA_ATTR static uint8_t seq; // 送信SEQ
Adafruit_BME280 bme;
uint16_t temp; // 温度
uint16_t humid; // 湿度
uint16_t press; // 気圧
uint16_t vbat; // 電圧
void setAdvData(BLEAdvertising *pAdvertising) { // アドバタイジングパケットを整形する
BLEAdvertisementData oAdvertisementData = BLEAdvertisementData();
oAdvertisementData.setFlags(0x06); // BR_EDR_NOT_SUPPORTED | General Discoverable Mode
// oAdvertisementData.setFlags(0x05); // BR_EDR_NOT_SUPPORTED | Limited Discoverable Mode
std::string strServiceData = "";
strServiceData += (char)0x0c; // 長さ(12Byte)
strServiceData += (char)0xff; // AD Type 0xFF: Manufacturer specific data
strServiceData += (char)0xff; // Test manufacture ID low byte
strServiceData += (char)0xff; // Test manufacture ID high byte
strServiceData += (char)seq; // シーケンス番号
strServiceData += (char)(temp & 0xff); // 温度の下位バイト
strServiceData += (char)((temp >> 8) & 0xff); // 温度の上位バイト
strServiceData += (char)(humid & 0xff); // 湿度の下位バイト
strServiceData += (char)((humid >> 8) & 0xff); // 湿度の上位バイト
strServiceData += (char)(press & 0xff); // 気圧の下位バイト
strServiceData += (char)((press >> 8) & 0xff); // 気圧の上位バイト
strServiceData += (char)(vbat & 0xff); // 電池電圧の下位バイト
strServiceData += (char)((vbat >> 8) & 0xff); // 電池電圧の上位バイト
oAdvertisementData.addData(strServiceData);
pAdvertising->setAdvertisementData(oAdvertisementData);
}
void setup() {
M5.begin();
M5.Axp.ScreenBreath(10); // 画面の輝度を下げる
M5.Lcd.setRotation(1); // LCDの方向を変える
M5.Lcd.setTextSize(2); // フォントサイズを2にする
M5.Lcd.setTextColor(WHITE, BLACK); // 文字を白、背景を黒
Wire.begin(); // I2Cの初期化
while (!bme.begin(0x76)) { // BME280の初期化
M5.Lcd.println("BME280 init failed"); // 初期化失敗
}
temp = (uint16_t)(bme.readTemperature() * 100); // 温度の取得(100倍して小数点以下を整数部へ)
humid = (uint16_t)(bme.readHumidity() * 100); // 湿度の取得(100倍して小数点以下を整数部へ)
press = (uint16_t)(bme.readPressure()/100); // 気圧の取得(pa→hPaに変換」)
vbat = (uint16_t)(M5.Axp.GetVbatData() * 1.1 / 1000 * 100); // バッテリーの電圧
M5.Lcd.setCursor(0, 0, 1); // カーソル位置
// 温度、湿度、気圧、バッテリーの電圧、MACアドレスをディスプレイに表示(検証時のみ表示する)
M5.Lcd.printf("temp: %4.1f'C\r\n", (float)temp / 100);
M5.Lcd.printf("humid:%4.1f%%\r\n", (float)humid / 100);
M5.Lcd.printf("press:%4.0fhPa\r\n", (float)press);
M5.Lcd.printf("vbat: %4.2fV\r\n", (float)vbat / 100);
BLEDevice::init("blepub-01"); // デバイスを初期化
BLEServer *pServer = BLEDevice::createServer(); // サーバーを生成
BLEAdvertising *pAdvertising = pServer->getAdvertising(); // アドバタイズオブジェクトを取得
setAdvData(pAdvertising); // アドバタイジングデーターをセット
pAdvertising->start(); // アドバタイズ起動
delay(T_PERIOD * 1000); // T_PERIOD秒アドバタイズする
pAdvertising->stop(); // アドバタイズ停止
seq++; // シーケンス番号を更新
delay(10);
esp_deep_sleep(1000000LL * S_PERIOD); // S_PERIOD秒Deep Sleepする
}
void loop() {
}
補足説明
送信とDeep Sleep
T_PERIODでアドバタイジング(ブロードキャスト)する秒数(10秒)を指定している。
またS_PERIODでDeepSleepする秒数(20秒)を指定している。
このプログラムでは10秒送信して20秒間Sleepにしているが5分間隔で温湿度、気圧を測定するのであれば4分50秒(250秒)と用途に応じて変更する。
またESP32には下記の5のモードがある。
- Active mode:100mA~240mA(動作状況による)
- Modem-sleep mode:20mA~25mA(Single-core)
- Light-sleep mode:0.8mA(800µA)
- Deep-sleep mode:10µA~150µA
- Hibernation mode:5µA
下から2番目のDeep Sleepモードだと消費電力がかなり少ない事が分かる。
今回のプログラムではDeep Sleep に esp_deep_sleep() を使っている。
サンプルプログラムでは esp_sleep_enable_timer_wakeup() でwakeup(起動)するまでの時間を指定して esp_deep_sleep_start() でDeep Sleep を開始する例が良く載っている。
esp_deep_sleep() では内部で esp_sleep_enable_timer_wakeup() と esp_deep_sleep_start() を連続して呼んでいるので、処理的には同じ内容になる。
Deep Sleep 復帰後は setup() が実行されるので loop() に処理は書かずに setup() に全てのロジックを書いている。
しかし Deep Sleep が不要であれば温湿度を取得してディスプレイに表示やアドバタイジングの処理は loop() 内に書いても良い。
setAdvData
アドバタイジングするデータを組み立てている。
M5StickCはBluetooth4.2なので送信可能なメッセージサイズは31Byteまでとなる。
アドバタイジングデータのレイアウトは以下の通り(Bluetooth.comより引用)
- Length:データ長(1オクテット)AD Type から電池電圧の上位バイトまで合計 12Byte なので16進数で 0x0c を指定している
- AD Type:AD Type+会社ID(3オクテット)
- AD Data:SEQ、温度、湿度、気圧、電圧(9オクテット)
から構成している。
AD Typeの0xffはメーカ固有データを表しており、後続2オクテット(2Byte)が会社IDを表している。
会社IDは今回はダミーの0xff×2を使用したが本格的に使用するのであればBluetooth SIG(Special Interest Group)に参加してCompany IDを申請して取得する必要がある。
リトルエンディアン
温度、湿度などの値(2オクテット)は上位、下位を逆転したリトルエンディアンの形式で保存している。
リトルエンディアンについては以前の記事を参照して欲しい。
M5.Lcd.printf
62~65行目でM5StickCのディスプレイに温湿度、気圧、電圧を表示して値を確認出来るようにしている。
リチウム電池を長持ちさせたいのであればこの表示は不要で例えばボタンを押したときだけ表示するように変更すれば良い。
ラズパイ4
データを受け取る側のRaspberry Pi 4Bのプログラムは以下の通り。
ソースコード
ble_sub.py
# -*- coding: utf-8 -*-
# M5StickCにBME280を接続
# 温度、湿度、気圧、電圧を取得してBLEでアドバタイズ(ブロードキャスト)
# M5StickCは10秒:アドバタイズ、20秒:Deep Sleep
# ラズパイ側は常時スキャンし、データーを取得したらprintする
from bluepy.btle import DefaultDelegate, Scanner, BTLEException
import sys
import struct
from datetime import datetime
class ScanDelegate(DefaultDelegate):
def __init__(self): # コンストラクタ
DefaultDelegate.__init__(self)
self.lastseq = None
self.lasttime = datetime.fromtimestamp(0)
def handleDiscovery(self, dev, isNewDev, isNewData):
if isNewDev or isNewData: # 新しいデバイスまたは新しいデータ
for (adtype, desc, value) in dev.getScanData(): # データの数だけ繰り返す
if desc == 'Manufacturer' and value[0:4] == 'ffff': # テスト用companyID
__delta = datetime.now() - self.lasttime
# アドバタイズする10秒の間に複数回測定されseqが加算されたものは捨てる(最初に取得された1個のみを使用する)
if value[4:6] != self.lastseq and __delta.total_seconds() > 11:
self.lastseq = value[4:6] # Seqと時刻を保存
self.lasttime = datetime.now()
(temp, humid, press, volt) = struct.unpack('<hhhh', bytes.fromhex(value[6:])) # hは2Byte整数(4つ取り出す)
print('温度= {0} 度、 湿度= {1} %、 気圧 = {2} hPa、 電圧 = {3} V'.format( temp / 100, humid / 100, press, volt/100))
if __name__ == "__main__":
scanner = Scanner().withDelegate(ScanDelegate())
while True:
try:
scanner.scan(5.0) # スキャンする。デバイスを見つけた後の処理はScanDelegateに任せる
except BTLEException:
ex, ms, tb = sys.exc_info()
print('BLE exception '+str(type(ms)) + ' at ' + sys._getframe().f_code.co_name)
補足説明
while文でループをしながらスキャンをしてM5StickCでアドバタイジングされたデータを取り出している。
データを受信するとScanDelegateクラスのhandleDiscoveryメソッドが呼ばれるので会社IDが0xffffのデータのみを抽出している。
27行目のstruct.unpackでパックされたバイナリーデータを取り出している。
“<“はリトルエンディアンを表していて、”h”は整数(2Byte)を表している。
実行結果
プログラムを書き込みM5StickCの電源をオンにするとアドバタイジングがすぐに始まると同時にディスプレイに温湿度、気圧、電圧の情報が表示される。
bluetoothctl
ラズパイ4側のプログラムを動かす前にLXTerminalから対話型のbluetoothctlコマンドでデータが受信できているかを確認する。
bluetoothctl
show
scan on
- show:情報の表示
- scan on:情報のスキャン
- quit:終了する
Keyが0xffff(会社ID)のデータが今回のプログラムからのデータなので正常に受信できていることが分かる。
ラズパイ4のプログラム
RaspberryPi 4Bのプログラムを実行する。
注意点としてはBluetoothでデバイスにアクセスする為にはroot権限が必要なのでsudoでプログラムを起動する必要がある。
sudo python ble_sub.py
約30秒毎に受信した温湿度等の情報をコンソールに表示している。
到達距離について
続いてM5StickCを少し離れた所に置いてデータを受信できるかどうかを試してみた。
見通しの良いところであれば数十メートル(100m弱)離れても問題なく受信できた。
ただオフィス内の扉や壁、キャビネットの配置状況によっては状況が異なってくる。
多少の壁があっても15m程度であれば問題なく受信が出来たが鉄筋コンクリートの壁越しだと届かない事もあった。
しかし初期の頃のBluetoothよりかは格段に「遠くまで届いている感」を感じられたのは確かだ。
これだけ届くのであればIoT機器同士の通信ではかなりの威力を発揮するのではと感じた。
最後に
今回はM5StickCとラズパイ4と1対1の組み合わせでテストを行ったが複数のM5StickC(センサー)を配置してラズパイは電源が確保できる所に1台だけ配置して複数のセンサーの情報を収集してまとめてクラウドにアップロードする構成でも面白いのではと感じた。
例えば畑やハウスなどの屋外では全ての場所での電源の確保が難しいので複数箇所に配置したM5StickCはリチウム電池+補助バッテリーで稼働させてRaspberry Piに一旦データを集約させて集計するなどの使用方法は可能性を感じた。
動画
このブログの内容を動画にしているので実際に表示している様子は以下を参照して欲しい。
以上で今回の記事は終了とする。
この記事が何処かで誰かの役に立つことを願っている。
尚、当記事中の商品へのリンクはAmazonアソシエイトへのリンクが含まれています。Amazonのアソシエイトとして、当メディアは適格販売により収入を得ていますのでご了承ください。
nice work, decent blog.
Thank you for your comment.
M5StickC is a great product.
A Writer from Japan.
いつも楽しく拝見させていただいています。今回、souichirouさんの記事で、工場の各工程にM5STACKとBME280センサーとダストセンサーを置いて、1台のラズパイでpygameでモニター表示させようと試行錯誤しております。そこで、質問なのですが、見通しの良いところは確かにBLE通信できております。少し障害物などがあるとBLE通信が届かなく、困っています。なので、M5STACKとラズパイの間にもう一台M5STACKを入れて、ゲートウェイとして通信を安定させたいと思いますが、追加のM5STACKに送受信するコードが分かりません。教えて頂けないでしょうか?よろしくお願いいたします。
fumi さん
コメントありがとうございます。
M5Stackをゲートウェイとして使った事は無いのでやってみないと分からない所ですが、
以下のサイトのコードが参考になるかも知れません。
https://ambidata.io/samples/m5stack/ble_gw/
リンク先のコードはM5Stackでスキャンした後、データが見つかった際に(23~28行目)ambientで送信していますが
ここで再びアドバタイジングするコードになるかと思います。
上手くいくかどうかはちょっと分かりません。
ラズパイをもう一台購入してBLEを受信できる位置に設置する方が確実かも知れません。
早々な対応をありがとうございます。
ゲートウェイのコードの件、何となくイメージが出来ましたので、早速試してみたいと思います。
また、成功・失敗がありましたら、御報告させていただこうかなと思います。
pythonやC++等、全くのド素人だった私は、souichirouさんの記事を元に、見よう見まねで少しは
出来るようになりました。本当に感謝いたします。
このBLEが成功すれば、この先はシグナルタワーの光源を取得して、CSV・グラフ表示したいと思って
おります。また、今後とも御教示願います。
いつも、拝見させていただきています。説明が長くなり申し訳ありません。以前からM5STACKからBLEにてラズパイにデータを送信していたのですが、最近作成したプログラムの中で温湿度センサーをM5STACKに接続し、プログラムはほぼコピペでプログラムを流した時に、データがラズパイ側に送信されず困っております。ラズパイ側のBLUETOOTHCTLでSCAN ONした時に、以前作成した物は「ManufacturerData」として受信できますが、RSSI:-59やTxPower:3等の電波?しか表示されません。また、value[0:4]の内容も正常なら数字4桁でIF文で判別できますが、valueの0桁目からBlepub-01が入っている?みたいです。プログラムは、ほぼ参考にさせていただいていますので、間違いないかと思いますが、他に注意しなければならない点があれば教えて頂けないでしょうか?ちなみに以前作成したプログラムも新たにコンパイルし書き込みなおすと送信できないことが確認できています。よろしくお願いします。
fumi さん
おはようございます。
今、M5Stick-C と RaspberryPi4B で試してみましたがこちらの環境では問題なく動作するようです。
※M5Stackでは試していません
状況からしてBLEでアドバタイジングする前に何らかのエラーになってしまっているのでは無いでしょうか。
BME280から正しく温湿度を取得出来ていない(接触不良、壊れた)等の理由です。
1ステップごとにprint文を入れて状況を確認してみたらいかがでしょうか。
またM5Stack ATOM LiteでもBLEを使っていますが、基本的に同じコーディングです。
https://www.souichi.club/m5stack/m5stack-optical-sensor/
返信ありがとうございます。昨日一日悩みましたが、解決しました。souichirouさんのプログラムを参考に非接触温度センサーと気温湿度センサーの値を取得しようとした時に、データの長さが問題だったようで、0cを0aに修正したら送信できました。長さは0c以内なら問題ないと思いこんでおりましたので、大変勉強になりました。ありがとうございま
す
データ長の指定の問題だったのですね。
解決して何よりです。
ピンポイントで欲しい情報が載ってて本当に助かりました!
私の場合deepsleepさせる必要はなく,センサの値を連続的に取得・送信したかったので,センサ値の取得とBLEDevice::createServer()以降の処理をloop()内に収めました。それでも問題なく動いています!
p3ishnm2さん
deepsleepすると毎回setup()が動いてしまいますからね。
loop()内でdelayを使えば等間隔で値が取得できると思います。
記事にも追記しておきます。
コメントありがとうございます?