見出し画像

プログラムと電子工作・Bluetoothセンサを接続して、フィットネスバイクのクランク回転数を測る(1)

高齢者の介護予防には下肢筋力を維持する運動が欠かせません。転倒リスクがなく、テレビを見ながら’ながら運動’するにはフィットネスバイク(エルゴメーター)が適しています。安価なエルゴメーターにサイクリング用のケイデンスセンサを取付け、1日の回転数を計測します。計測結果を LINEなどで共有すれば、本人の励みになるし、家族の見守りになります。

電子工作的には、フィットネスバイクのクランクに、クランクの回転数を計測するケイデンスセンサを取り付けるだけで、特段の工作はありません。

プログラム的には、ケイデンスセンサと M5StickC Plus を Bluetooth で接続し、回転数データを M5StickC Plus に記録するだけです。

図1 部品構成

目標

  • ケイデンスセンサと M5StickC Plus を Bluetooth で接続します。

  • クランク回転数を M5StickC Plus に表示します。

部品・機材

使用する部品は次のとおりです。

電子部品

  • M5StickC Plus 1台

  • XOSS Vortex ケイデンス&スピードセンサ
    ※この製品では Bluetooth接続を確認できました。

  • フィットネスバイク(エルゴメーター)
    ※フィトネスバイクはなくてもプログラムの動作テストは可能です。

開発用機材

  • PC(Windows10 または 11)、開発環境 Arduino-IDE 導入ずみ

  • USB-A・USB-C ケーブル

開発手順

  1. PC と M5StickC Plus を USBケーブルで接続する。

  2. Arduino-IDE でスケッチ blesensor.ino を開く。

  3. 検証・コンパイルする。

  4. M5StickC Plus に書き込む。

  5. ケイデンスセンサをフィットネスバイクのクランクに取り付け、クランクを回す。

Bluetooth の仕組み

ケイデンスセンサからのデータを M5StickC Plus で受信する構成では、ケイデンスセンサが BLE ServerM5StickC Plus BLE Client になります。

NimBLE-Arduino のドキュメントを読み解きましょう。NimBLE-Arduino/docs
/New_user_guide.md の Creating a Client の章
です。

BLE Client は次の機能を実行します。

  1. BLE Server を探す

  2. 該当する BLE Server と接続する

  3. characteristics の値(value)と property、descriptors を読み書きする

characteristics の値は最も重要なデータ、property は characteristics の属性、descriptors は補足情報です。

characteristics には readwritenotify の 3種類の property があります。readwrite は BLE Client のタイミングで読み書きしますが、notify は BLE Server のタイミングで BLE Client に通知が届きます。

1. BLE Server を探す

まず近くにあるすべての BLE Server を抽出します。

NimBLEScanResults NimBLEScan::start(uint32_t duration);
// return: NimBLEScanResults スキャン結果
// in: uint32_t duration スキャン時間(秒)
// 注意:この関数はスキャン時間ブロックする。

Github のサンプルスケッチでは duration スキャン時間の単位は「ミリ秒」となっていますが、「秒」が正しいようです。

#include "NimBLEDevice.h"

// void setup() in Arduino
void app_main(void)  
{
    NimBLEDevice::init("");
    
    NimBLEScan *pScan = NimBLEDevice::getScan();
    NimBLEScanResults results = pScan->getResults(10);  /*スキャン時間 10秒*/
}

2. BLE Server と接続する

スキャン結果から、目的のサービスを見つけます。

例えば、欲しいサービスの UUID を「ABCD」とすると、目的のサービスが見つかったら、そのデバイスと接続する BLE Client を生成し、通信を開始します。処理がすべて終わったら、BLE Client を消去します。

NimBLEUUID serviceUuid("ABCD");

for(int i = 0; i < results.getCount(); i++) {
    NimBLEAdvertisedDevice device = results.getDevice(i);
    
    if (device.isAdvertisingService(serviceUuid)) {
        //  --- found target service
        //  create a client and connect
        NimBLEClient *pClient = NimBLEDevice::createClient();
        
        if (pClient->connect(&device)) {
            //  --- success
            //  read the characteristic value
        }
        else {
            //  --- failed to connect
        }
        
        NimBLEDevice::deleteClient(pClient);
    }
}

3. characteristics の値を読む

BLE Server と通信中は、characteristics の UUID(例えば「1234」)を用いて、characteristics の値を読みます。上のサンプルスケッチの // read the characteristic value の箇所です。

//  --- success
NimBLEUUID characteristicsUuid("1234");
NimBLERemoteService *pService = pClient->getService(serviceUuid);
            
if (pService != nullptr) {
    NimBLERemoteCharacteristic *pCharacteristic = pService->getCharacteristic(characteristicsUuid);

    if (pCharacteristic != nullptr) {
        std::string value = pCharacteristic->readValue();
        //  print or do whatever you need with the value
    }
}

UUID の割当て

UUID の値は、Bluetooth の Webサイトで調べることができます。例えば、ケイデンスセンサのサービス UUID は、「Cycling Speed and Cadence service」で調べると、0x1816 です。またケイデンスセンサの characteristics の値の UUID は、「CSC Measurement」(おそらく Cycling Speed and Cadence で CSC、測定値で Measurement)が見つかり、0x2A5B です。

前述のサンプルスケッチでは、「ABCD」を「1816」に、「1234」を「2A5B」に書き換えればいいはずです。

characteristics のデータ構造

前述のサンプルスケッチで読み取った std::string value はどんな構造でしょうか。文字列であることは分かります。

std::string value = pCharacteristic->readValue();

データ構造は下表のようになります。個々の値は Little Endian です。(参考文献1 参照)

図2 データ構造

スケッチ

blesensor.ino

#include <M5Unified.h>
#include "NimBLEDevice.h"

NimBLEUUID serviceUuid("1816");
NimBLEUUID characteristicsUuid("2A5B");

void setup() {
    //  電源ON時に 1回だけ実行する処理をここに書く。
    M5.begin();             /*M5を初期化する*/
    M5.Lcd.setTextSize(2);  /*文字サイズはちょっと小さめ*/
    M5.Lcd.setRotation(3);  /*上スイッチが左になる向き*/

    Serial.begin(115200);

    M5.Lcd.print("blesensor");
    delay(2000);
}

void loop() {
    //  自動的に繰り返し実行する処理をここに書く。
    Serial.println("*** scanning...");
    NimBLEDevice::init("");
    NimBLEScan *pScan = NimBLEDevice::getScan();
    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
            //  create a client and connect
            NimBLEClient *pClient = NimBLEDevice::createClient();

            if(pClient->connect(&device)) {
                //  --- success
                //  read the characteristic value
                NimBLERemoteService *pService = pClient->getService(serviceUuid);
                std::string service = std::string(pService->getUUID());
                Serial.printf("*** service value: %s\n", service.c_str());

                if (pService != nullptr) {
                    NimBLERemoteCharacteristic *pCharacteristic = pService->getCharacteristic(characteristicsUuid);

                    if (pCharacteristic != nullptr) {
//  文字列として扱うと英数字記号以外が表示されない。
//  NimBLE-Arduino/src/NimBLEAttValue.h を読み解いて、下記のとおり修正する。
//                        std::string value = pCharacteristic->readValue();
//                        //  print or do whatever you need with the value
//                        Serial.printf("*** characteristic value: %s\n", value.c_str());
                        NimBLEAttValue charData = pCharacteristic->getValue();
                        const uint8_t* value = charData.data();
                        const uint16_t len = charData.length();

                        //  Characteristic の値を 16進表記で出力する。
                        std::string str = "*** characteristic value";
                        str += "[" + std::to_string(len) + "]: ";
                        char buf[4];
                        for (uint16_t i = 0; i < len; i++) {
                            snprintf(buf, sizeof(buf), "%02x ", value[i]);
                            str += std::string(buf);
                        }
    
                        Serial.println(str.c_str());
                    }
                }
            }
            else {
                //  --- failed to connect
            }

            NimBLEDevice::deleteClient(pClient);
        }
    }

    //  30秒に 1回繰り返す。
    delay(30000);
}

結果

ケイデンスセンサをクルクル回してやると自動的に電源がONし、M5StickC Plus と通信を開始します。

写真1 ケイデンスセンサ(クルクル回す治具を段ボールで製作)

Arduino-IDE のシリアルモニタには下記のような結果が表示されます。

*** scanning...
*** device found: Name: , Address: 11:24:90:3e:b6:ed, manufacturer data: 0600010920228b8fc26683ed28f69e3dad41ec62a62458932d4af09f4c
*** 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
*** characteristic value[0]: 
*** device found: Name: , Address: 70:95:14:2a:d1:67, manufacturer data: 4c001608005b869cc4f5b776
*** device found: Name: , Address: 12:6a:22:e0:0d:4c, manufacturer data: 060001092022d8f7cebe508176f6e8e2fd2e53ad816f4527821b9910fa

4~6行目がケイデンスセンサのものです。「XOSS_VOR_C2145」という記述が見えます。回転数のデータを期待したのですが characteristic value は空です。

characteristics には readwritenotify の 3種類の property があり、上記の結果は read のものです。センサデータを定期的に読む read では、センサデータを読めないのかもしれません。notify を試してみましょう。

次はセンサのデータが更新されたときだけ通信が発生する notify で通信する方式を組込みます。

スケッチ

blesensor2.ino

#include <M5Unified.h>
#include "NimBLEDevice.h"

NimBLEUUID serviceUuid("1816");
NimBLEUUID characteristicsUuid("2A5B");

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("blesensor2");
    delay(2000);
}

void loop() {
    //  自動的に繰り返し実行する処理をここに書く。
    static bool isConnected = false;

    if (!isConnected) {
        Serial.println("*** scanning...");
        NimBLEDevice::init("");
        NimBLEScan *pScan = NimBLEDevice::getScan();
        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
                //  create a client and connect
                NimBLEClient *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
                                Serial.println("*** subscribe succeeded.");
                                isConnected = true;
                                break;  /*break out from for ()*/
                            }
                            else {
                                //  --- failed
                                /** Disconnect if subscribe failed */
                                Serial.println("*** subscribe failed.");
                                pClient->disconnect();
                                isConnected = false;
                            }
                        } 
                    }
                }
                else {
                    //  --- failed to connect
                }

                NimBLEDevice::deleteClient(pClient);
            }
        }
    }

    //  30秒に 1回繰り返す。
    delay(30000);
}

/**
 *  サンプルスケッチ NimBLE-Arduino/examples/NimBLE_Client/NimBLE_Client.ino を参照
 */
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());

    //  ホイール回転数データかクランク回転数データかの別を判定する。
    if (pData[0] & 0x01) {
        //  --- ホイール回転数データ
        uint32_t rev =  ((uint32_t)pData[1])        + (((uint32_t)pData[2]) << 8)
                     + (((uint32_t)pData[3]) << 16) + (((uint32_t)pData[4]) << 24);
        uint32_t tim = (uint32_t)pData[5] + (((uint32_t)pData[6]) << 8);
        uint32_t tic = millis();
        Serial.printf("Wheel revolution = %u, Last event time = %u, Last event tick = %u\n",
                      rev, tim, tic);
    }
    else if (pData[0] & 0x02) {
        //  --- クランク回転数データ
        uint32_t rev = ((uint32_t)pData[1]) + (((uint32_t)pData[2]) << 8);
        uint32_t tim = (uint32_t)pData[3] + (((uint32_t)pData[4]) << 8);
        uint32_t tic = millis();
        Serial.printf("Crank revolution = %u, Last event time = %u, Last event tick = %u\n",
                      rev, tim, tic);
    }
}

notifyCB() 関数は、BLE Server からの notify を受け取ったときに呼び出す Callback関数です。ここでは、クランク回転数と notify が発生した時刻、CPU時刻をシリアル通信へ出力します。

blesensor.ino では該当するサービスと characteristics が見つかったときに getValue() で characteristics value を読み取っていましたが、blesensor2.ino では該当する characteristics に Callback関数を subscribe(true, notifyCB) で登録します。

結果

ケイデンスセンサをクルクル回してやると自動的に電源がONし、M5StickC Plus と通信を開始し、次々に notify を受信します。

Arduino-IDE のシリアルモニタには下記のような結果が表示されます。

*** scanning...
*** device found: Name: , Address: 00:bb:c1:34:87:d9, manufacturer data: 4c0003161100000277c0a81462000000000000000000000000d0
*** device found: Name: , Address: 25:07:d0:40:cc:02, manufacturer data: 06000109202217867fe38a1bc87b994d50bc584df4491a565aca5d3d25
*** 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 29 00 13 80 
Crank revolution = 41, Last event time = 32787, Last event tick = 94934
*** Notification from ce:10:12:6e:9e:be: Service = 0x1816, Characteristic = 0x2a5b, Value[5] = 02 2a 00 f5 84 
Crank revolution = 42, Last event time = 34037, Last event tick = 95934
*** Notification from ce:10:12:6e:9e:be: Service = 0x1816, Characteristic = 0x2a5b, Value[5] = 02 2a 00 f5 84 
Crank revolution = 42, Last event time = 34037, Last event tick = 96934
*** Notification from ce:10:12:6e:9e:be: Service = 0x1816, Characteristic = 0x2a5b, Value[5] = 02 2b 00 d9 89 
Crank revolution = 43, Last event time = 35289, Last event tick = 97934
*** Notification from ce:10:12:6e:9e:be: Service = 0x1816, Characteristic = 0x2a5b, Value[5] = 02 2c 00 df 8e 
Crank revolution = 44, Last event time = 36575, Last event tick = 98935
*** Notification from ce:10:12:6e:9e:be: Service = 0x1816, Characteristic = 0x2a5b, Value[5] = 02 2d 00 dc 93 
Crank revolution = 45, Last event time = 37852, Last event tick = 99985

BLE Server をスキャンして、4行目で「XOSS_VOR_C2145」で、ケイデンスセンサを見つけます。6行目「subscribe succeeded.」で notify Callback関数を subscribe します。

その後はケイデンスセンサからの notify を受信するごとに characteristics の値を表示します。Crank revolution、Last event time の値はケイデンスセンサから notify された値、Last event tick は millis()関数で求めた値です。

Last event tick を読むと、約 1秒ごとに notify を受信していることが分かります。Crank revolution の値でクランクの回転数が計算できます。Last event tick と組み合わせて、ケイデンス(単位時間あたりのクランク回転数)を計算できます。

参考

参考文献1

ライセンス

このページのソースコードは、複製・改変・配布が自由です。営利目的にも使用してかまいませんが、何ら責任を負いません。

いいなと思ったら応援しよう!