プログラムと電子工作・Bluetoothセンサを接続して、フィットネスバイクのクランク回転数を測る(2)
「Bluetoothセンサを接続して、フィットネスバイクのクランク回転数を測る(1)」で、Bluetooth センサ・M5StickC Plus の通信ができたので、フィットネスバイクのクランク回転数計測はおおよそできそうです。残る機能は積算した値を LCDディスプレイに書くだけですが、いくつか懸念があります。
setup() や loop()関数と Bluetooth通信の Callback関数 notifyCD() で、変数を読み書きするとき不整合が発生しないか。
ケイデンスセンサの電源OFFなどで、Bluetooth通信が切れた後、再接続できるか。
M5StickC Plus 内蔵の ESP32プロセッサは 2つのコアを持ちます。loop()内、notifyCB()内にそれぞれ下記のコードを挿入して、シリアルモニタ出力を観察すると、loop()は CoreID: 1で、notifyCB()は CoreID: 0で同時並行して動作していることが分かります。
/* loop() 内 */
Serial.print("*** in loop()...CoreID: ");
Serial.println(xPortGetCoreID());
/* notifyCB() 内 */
Serial.print("*** in notifyCB()...CoreID: ");
Serial.println(xPortGetCoreID());
loop()と notifyCB()で同じ変数にアクセスすると、タイミングによっては書き込み途中の値を読み出してしまう不整合が発生します。これを防ぐには排他処理が必要になります。
M5StickC Plus のスケッチは通常 setup()と loop()の処理を書きます。実際は Wi-Fi、Bluetoothやシリアルなどの通信などを見えないところで処理しています。見えない処理の動作・停止や動作の優先順位を管理しているのがリアルタイムOSで、M5Stackシリーズでは FreeRTOS を実装しています。
notifyCB()は、この見えない処理から呼び出される関数です。見えない処理の制御にはリアルタイムOSの機能を使用する必要があります。排他処理には Mutex を使用します。
// mutexを生成する。
SemaphoreHandle_t shared_var_mutex = xSemaphoreCreateMutex();
// 排他制御
if (shared_var_mutex != NULL &&
xSemaphoreTake(shared_var_mutex, portMAX_DELAY) == pdTRUE) {
// 変数にアクセスする。
...
xSemaphoreGive(shared_var_mutex);
}
ケイデンスセンサは、回転が止まると自動的に電源OFFします。同時に Bluetooth接続も切れてしまいます。ケイデンスセンサが再び回転を検出すると自動的に電源ONしますが、Bluetooth接続は切れたままです。再接続の機能が必要になります。
ケイデンスセンサからの notify は約 1秒ごとに発生しますので、規定時間 notify が届かないとき、ケイデンスセンサが電源OFFし、Bluetooth接続が切れたことが判定できます。
Bluetooth接続が切れたら、BLE Server を探すスキャンを開始します。
上ボタン(押しボタンA)を押すと、クランク回転数の積算を 0にリセットする機能も加えましょう。
これらの機能を blesensor2.ino に追加します。かなり大きな変更になります。
目標
クランク回転数の積算値を M5StickC Plus の LCDディスプレイに表示します。
上ボタン(押しボタンA)でクランク回転数をリセットします。
部品・機材
使用する部品は次のとおりです。「Bluetoothセンサを接続して、フィットネスバイクのクランク回転数を測る(1)」と同様です。
電子部品
M5StickC Plus 1台
※M5StickC 無印でも Plus2でも動作します。XOSS Vortex ケイデンス&スピードセンサ
フィットネスバイク(エルゴメーター)
※フィトネスバイクはなくてもプログラムの動作テストは可能です。
開発用機材
PC(Windows10 または 11)、開発環境 Arduino-IDE 導入ずみ
USB-A・USB-C ケーブル電子部品
開発手順
PC と M5StickC Plus を USBケーブルで接続する。
Arduino-IDE でスケッチ blesensor3.ino を開く。
検証・コンパイルする。
M5StickC Plus に書き込む。
ケイデンスセンサをフィットネスバイクのクランクに取り付け、クランクを回す。
スケッチ
blesensor3.ino
#include <M5Unified.h>
#include "NimBLEDevice.h"
#define LHEIGHT 15 /*1行の高さ(px)TextSize(3):25、TextSize(2):15*/
#define NOTIFY_TIMEOUT_TICK (5*1000) /*5秒、Bluetoothが切断されたと判定するnotifyが届かない時間(tick)*/
SemaphoreHandle_t shared_var_mutex = NULL; /*A*/
NimBLEUUID serviceUuid("1816"); /*スピード&ケイデンスセンサのサービス UUID*/
NimBLEUUID characteristicsUuid("2A5B"); /*ケイデンスデータの UUID*/
NimBLEScan* pScan = nullptr; /*B*/
NimBLEClient* pClient = nullptr; /*B*/
bool isConnected = false; /*ケイデンスセンサとの接続状態、true 接続中*/ /*B*/
uint32_t current_crank_count = 0; /*現在のクランク回転数の積算値(回)*/ /*A*/
uint32_t printed_crank_count = 0; /*表示のクランク回転数の積算値(回)*/ /*A*/
uint32_t last_notify_rev = 0; /*前回のnotify受信 Cumulative Crank Revolutions*/ /*A*/
uint32_t last_notify_tick = 0; /*前回のnotify受信 tick*/ /*A*/
void notifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify);
void setup() {
// 電源ON時に 1回だけ実行する処理をここに書く。
M5.begin(); /*M5を初期化する*/
M5.Lcd.setTextSize(2); /*文字サイズはちょっと小さめ*/
M5.Lcd.setRotation(3); /*上スイッチが左になる向き*/
Serial.begin(115200);
M5.Lcd.print("blesensor3");
// mutexを生成する。/*A*/
shared_var_mutex = xSemaphoreCreateMutex();
// Bluetooth 準備 /*C*/
NimBLEDevice::init("");
pScan = NimBLEDevice::getScan();
delay(2000);
}
void loop() {
// 自動的に繰り返し実行する処理をここに書く。
M5.update(); /*A*/
// 上ボタン(押しボタンA)をチェックする。/*A*/
if (M5.BtnA.wasPressed()) {
// --- 上ボタンが押された。
// 排他処理、mutexを獲得する。
if (shared_var_mutex != NULL &&
xSemaphoreTake(shared_var_mutex, portMAX_DELAY) == pdTRUE) {
// クランク回転数の積算値をリセットする。
current_crank_count = 0;
// mutexを返却する。
xSemaphoreGive(shared_var_mutex);
}
}
// 排他処理、mutexを獲得する。/*A*/
if (shared_var_mutex != NULL &&
xSemaphoreTake(shared_var_mutex, portMAX_DELAY) == pdTRUE) {
// クランク回転数の積算値、前回のnotify受信tickを一時記憶する。
uint32_t cc = current_crank_count; /*F*/
uint32_t nt = last_notify_tick; /*E*/
// mutexを返却する。
xSemaphoreGive(shared_var_mutex);
// 更新されていたら、クランク回転数を LCDディスプレイに書く。/*F*/
if (cc != printed_crank_count) {
M5.Lcd.setCursor(0, LHEIGHT*1); /*2行目*/
M5.Lcd.printf("crank_count: %6u", cc);
printed_crank_count = cc;
}
// notifyがタイムアウトしていたら、Bluetoothが切断されたとみなす。/*E*/
if (NOTIFY_TIMEOUT_TICK < millis() - nt) {
isConnected = false;
Serial.println("*** bluetooth disconnected.");
}
}
if (!isConnected) {
// --- BLEセンサが未接続。
M5.Lcd.setCursor(0, LHEIGHT*2); /*3行目*/
M5.Lcd.print("scanning... ");
Serial.println("*** scanning...");
NimBLEScanResults results = pScan->start(10); /*スキャン時間 10秒*/
for (int i = 0; i < results.getCount(); i++) {
NimBLEAdvertisedDevice device = results.getDevice(i);
Serial.printf("*** device found: %s\n", device.toString().c_str());
if (device.isAdvertisingService(serviceUuid)) {
// --- found target service
// 古い Clientが残っていたら、消去する。/*D*/
if (!pClient) {
NimBLEDevice::deleteClient(pClient);
pClient = nullptr;
}
// create a client and connect
pClient = NimBLEDevice::createClient();
if (pClient->connect(&device)) {
// --- success
NimBLERemoteService* pService = pClient->getService(serviceUuid);
std::string service = std::string(pService->getUUID());
Serial.printf("*** service value: %s\n", service.c_str());
// notify Callback関数を登録する。
if (pService != nullptr) {
NimBLERemoteCharacteristic *pCharacteristic = pService->getCharacteristic(characteristicsUuid);
/* サンプルスケッチ NimBLE-Arduino/examples/NimBLE_Client/NimBLE_Client.ino を参照 */
if (pCharacteristic != nullptr && pCharacteristic->canNotify()) {
if (pCharacteristic->subscribe(true, notifyCB)) {
// --- success
pScan->stop();
Serial.println("*** subscribe succeeded.");
uint32_t now = millis(); /*E*/
// 排他処理、mutexを獲得する。/*A*/
if (shared_var_mutex != NULL &&
xSemaphoreTake(shared_var_mutex, portMAX_DELAY) == pdTRUE) {
last_notify_rev = 0; /*F*/
last_notify_tick = now; /*E*/
isConnected = true;
// mutexを返却する。
xSemaphoreGive(shared_var_mutex);
}
break; /*break out from for ()*/
}
else {
// --- failed
/** Disconnect if subscribe failed */
Serial.println("*** subscribe failed.");
pClient->disconnect();
isConnected = false;
}
}
}
}
else {
// --- failed to connect
isConnected = false;
}
NimBLEDevice::deleteClient(pClient); /*Clientを消すと、notifyを受け取らない。*/
}
}
}
else {
// --- BLEセンサが接続ずみ。/*A*/
M5.Lcd.setCursor(0, LHEIGHT*2); /*3行目*/
M5.Lcd.print("btnA to clear count");
}
delay(1);
}
/**
* サンプルスケッチ NimBLE-Arduino/examples/NimBLE_Client/NimBLE_Client.ino を参照
*/
// BLE Serverからの notify を受信したときの Callback関数
void notifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify)
{
std::string str = (isNotify == true) ? "*** Notification" : "*** Indication";
str += " from ";
/** NimBLEAddress and NimBLEUUID have std::string operators */
str += std::string(pRemoteCharacteristic->getRemoteService()->getClient()->getPeerAddress());
str += ": Service = " + std::string(pRemoteCharacteristic->getRemoteService()->getUUID());
str += ", Characteristic = " + std::string(pRemoteCharacteristic->getUUID());
// str += ", Value = " + std::string((char*)pData, length);
// Serial.println(str.c_str());
// Characteristic の値を 16進表記で出力する。
char buf[4];
str += ", Value[" + std::to_string(length) + "] = ";
for (uint16_t i = 0; i < length; i++) {
snprintf(buf, sizeof(buf), "%02x ", pData[i]);
str += std::string(buf);
}
Serial.println(str.c_str());
// ホイール回転数データかクランク回転数データかの別を判定する。
uint32_t rev, tim, tic;
if (pData[0] & 0x01) {
// --- ホイール回転数データ
rev = ((uint32_t)pData[1]) + (((uint32_t)pData[2]) << 8)
+ (((uint32_t)pData[3]) << 16) + (((uint32_t)pData[4]) << 24);
tim = (uint32_t)pData[5] + (((uint32_t)pData[6]) << 8);
tic = millis();
Serial.printf("Wheel revolution = %u, Last event time = %u, Last event tick = %u\n",
rev, tim, tic);
}
else if (pData[0] & 0x02) {
// --- クランク回転数データ
rev = ((uint32_t)pData[1]) + (((uint32_t)pData[2]) << 8);
tim = (uint32_t)pData[3] + (((uint32_t)pData[4]) << 8);
tic = millis();
Serial.printf("Crank revolution = %u, Last event time = %u, Last event tick = %u\n",
rev, tim, tic);
}
// 排他処理、mutexを獲得する。5-ticks(5ミリ秒)で獲得できないときは保留する。/*A*/
if (shared_var_mutex != NULL &&
xSemaphoreTake(shared_var_mutex, (TickType_t)5) == pdTRUE) {
// 現在の tickを保存する。/*E*/
last_notify_tick = tic;
// クランク回転数を積算する。/*F*/
current_crank_count += rev - last_notify_rev;
last_notify_rev = rev;
// mutexを返却する。
xSemaphoreGive(shared_var_mutex);
}
}
blesensor2.ino からの変更点を記号 /*X*/ で示しています。
/*A*/ は、追加したステートメントです。
/*B*/ は、ローカル変数をグローバル変数に変更したステートメントです。
/*C*/ は、場所を移動したステートメントです。
/*D*/ は、メモリーリークが心配なので念のため追加しました。
/*E*/ は、Bluetooth接続が切れたことを検出する機能に関係するステートメントです。
/*F*/ は、クランク回転数の積算値に関係するステートメントです。
notify Callback関数 notifyCB()内でクランク回転数を積算するのと並行して、loop()関数で LCDディスプレイにクランク回転数の積算値を表示します。前者の関数が変数を書き換えている最中に、後者の関数が同じ変数を参照すると、不具合が発生する恐れがあります。Mutex を使用して排他制御を行います。排他処理している時間は、極力短くします。
blesensor.ino から順次機能を追加したため、スケッチが複雑化したように思います。Callback関数を活用してもう少しシンプルにできそうですが、サンプルスケッチ NimBLE-Arduino/examples/NimBLE_Client/NimBLE_Client.ino を相当読み解く必要がありそうです。
結果
とりあえずこんな感じで動きました。Arduino-IDE のシリアルモニタには下記のような結果が表示されました。
*** scanning...
*** device found: Name: , Address: 6f:91:6f:56:ef:f5, manufacturer data: 4c0010050318d823ea, txPower: 12
*** device found: Name: , Address: 5c:07:e1:ad:d6:49, manufacturer data: 4c0010062d1e67c307fc, txPower: 12
*** device found: Name: , Address: 05:1c:28:d5:2d:c1, manufacturer data: 060001092022282e5a0b51adac1e2860eeb22038cf4b8e9cc014cf4f05
*** device found: Name: , Address: d2:4d:a0:b6:2c:42, manufacturer data: 4c0012020003
*** device found: Name: Linksys, Address: e8:9f:80:93:49:39
*** bluetooth disconnected.
*** scanning...
*** device found: Name: , Address: 05:1c:28:d5:2d:c1, manufacturer data: 060001092022282e5a0b51adac1e2860eeb22038cf4b8e9cc014cf4f05
*** device found: Name: , Address: 5c:07:e1:ad:d6:49, manufacturer data: 4c0010062d1e67c307fc, txPower: 12
*** device found: Name: , Address: 6f:91:6f:56:ef:f5, manufacturer data: 4c0010050318d823ea, txPower: 12
*** device found: Name: , Address: d2:4d:a0:b6:2c:42, manufacturer data: 4c0012020003
*** device found: Name: Linksys, Address: e8:9f:80:93:49:39
*** device found: Name: XOSS_VOR_C2145, Address: ce:10:12:6e:9e:be, appearance: 1157, serviceUUID: 0x1816
*** service value: 0x1816
*** subscribe succeeded.
*** Notification from ce:10:12:6e:9e:be: Service = 0x1816, Characteristic = 0x2a5b, Value[5] = 02 05 00 47 21
Crank revolution = 5, Last event time = 8519, Last event tick = 803035
*** Notification from ce:10:12:6e:9e:be: Service = 0x1816, Characteristic = 0x2a5b, Value[5] = 02 06 00 68 24
Crank revolution = 6, Last event time = 9320, Last event tick = 804035
*** Notification from ce:10:12:6e:9e:be: Service = 0x1816, Characteristic = 0x2a5b, Value[5] = 02 07 00 72 27
Crank revolution = 7, Last event time = 10098, Last event tick = 805035
*** Notification from ce:10:12:6e:9e:be: Service = 0x1816, Characteristic = 0x2a5b, Value[5] = 02 09 00 58 2d
Crank revolution = 9, Last event time = 11608, Last event tick = 806085
*** Notification from ce:10:12:6e:9e:be: Service = 0x1816, Characteristic = 0x2a5b, Value[5] = 02 0a 00 16 30
Crank revolution = 10, Last event time = 12310, Last event tick = 807785
ケイデンスセンサの電源OFFからONに復帰したときのログです。BLE Serverのスキャン中にクランク回転数が増えたので、Crank revolution = 5 が notify の初回値になりました。
クランク回転数の積算値は、電源OFF以前の数値に加算するので、M5StickC Plus の LCD表示は 5より大きな数値になっています。
練習問題
blesensor3.ino は、BLE Server をスキャン中、上ボタン検出も notify 受信もできません。Callback関数を用いる方式に変更してみてください。
クランク回転数の積算値が一定数を超えたら、LINEへ通知する仕組みを実装してください。手前味噌ですが以下の記事が参考になります。
参考
排他制御
ライセンス
このページのソースコードは、複製・改変・配布が自由です。営利目的にも使用してかまいませんが、何ら責任を負いません。