家庭用サイクルトレーナーにmicro:bitをつなぐ（その４）- スマホ・アプリでケイデンスやスピードを表示できるようにBLEのGATTサービス(FTMP/FTMS)を実装する

# 家庭用サイクルトレーナーにmicro:bitをつなぐ（その４）
前回までの記事（[その１](https://qiita.com/jp-96/items/110b6364a3da89553b27)、[その２](https://qiita.com/jp-96/items/c494da04f04603ef258d)、[その３](https://qiita.com/jp-96/items/3863e31b1c92a9caa44b)）では、家庭用サイクルトレーナーのワイヤレス運動測量計とmicro:bitをつなぎ、MicroBitComponentを継承し、ワイヤレス運動測量計のSTEP信号からケイデンスとスピードを求めるモジュールをドライバー化したMicroBitIndoorBikeStepSensorクラスを作成しました。

**リンク**

1. [家庭用サイクルトレーナーにmicro:bitをつなぐ（その１）  
ワイヤレス運動測量計から出力されるクランク回転センサーの信号をモニターする](https://qiita.com/jp-96/items/110b6364a3da89553b27)
2. [家庭用サイクルトレーナーにmicro:bitをつなぐ（その２）  
STEP信号からワイヤレス運動測量計に表示されているスピードをmicro:bitで推定する](https://qiita.com/jp-96/items/c494da04f04603ef258d)
3. [家庭用サイクルトレーナーにmicro:bitをつなぐ（その３）  
STEP信号からケイデンスとスピードを求めるモジュールをドライバー（アイドル・コンポーネント）として作成する](https://qiita.com/jp-96/items/3863e31b1c92a9caa44b)

これまで、ケイデンスやスピードをUSBシリアル通信でモニターしていましたが、これをBLE通信でスマホ・アプリで表示できるようにGATTサービスであるFTMP/FTMSとして実装します。

![IMG_20210219_081420.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252891/a7141603-84e7-72dd-cad2-a814484aa186.jpeg)


# 仕様書の入手と斜め読み

https://www.bluetooth.com/specifications/specs/

> **FTMP** - Fitness Machine Profile 1.0
> **FTMS** - Fitness Machine Service 1.0

## FTMP - Fitness Machine Profile
まずは、仕様書を入手し、FTMP(Fitness Machine Profile)の方をサラッと読みましょう。
次のようなことが書かれています。

> Indoor Bike（室内バイク）のようなフィットネスマシンからデータを取得したり、制御したりできます。
> 実装するサービスは、`Fitness Machine Service`(必須)、`User Data Service`(任意)、`Device Information Service`(任意)の３つです。`Fitness Machine Service`において、その特性(Characteristics)として、`Fitness Machine Feature`(必須)や`Indoor Bike Data`(任意)、`Training Status`(必須)、`Fitness Machine Control Point`(任意)、`Fitness Machine Status`(任意)を実装します。
> `Fitness Machine Control Point`特性を実装した場合、`Procedure(Op Code)`の`Request Control`と`Reset`の実装は必須です。

## FTMS - Fitness Machine Service
続いて、FTMS(Fitness Machine Service)の方もサラッと読みましょう。
次のようなことが書かれています。

>Fitness Machine Service(FTMS)は、トレーニング関連のデータを提供したり(例えば、`Indoor Bike Data`特性)、トレーニング状況(`Training Status`特性)やマシン状態(`Fitness Machine Status`特性)を提供したりできます。また、`Fitness Machine Control Point`特性を公開することにより、マシンを遠隔操作できます。
>`Fitness Machine Feature`特性で、実装するサービスがサポートする状況に合わせたビット値を定義します。
>`Indoor Bike Data`特性は、`Flags Field`と各データ項目とで構成されており、`Flags Field`でサポート（提供）するデータ項目を定義します。
> ※`Instantaneous Speed Field`は、必ずデータ項目として含まれます。
> `Training Status`特性は、フラグとトレーニング状況及び場合によってはトレーニング状況に関する文字列で構成されます。
> `Fitness Machine Control Point`特性は、`Op Code`(1オクテット)と`Parameter`(0～18オクテット)とで構成されており、`Op Code`の`0x00(Request Control)`、`0x01(Reset)`、`0x07(Start or Resume)`、`0x08(Stop or Pause)`の実装は必須です。また、各要求に対して、`Op Code`の`0x80(Response Code)`と、`Request Op Code`と`Result Code`等を応答として返す必要があります。
> `Fitness Machine Control Point`特性を実装した場合、`Fitness Machine Status`特性も実装します。

# ケイデンスとスピードに対応したFTMSの実装

## ソースコード

ソースコードのビルドは、[Arm Mbed オンライン - https://ide.mbed.com/compiler/](https://ide.mbed.com/compiler/)で可能です。

Source URLに https://github.com/jp-96/microbit-digital-capture4.git と入力し、Programとしてインポートして、コンパイルします。

## 構成ファイル - mbed_app.json

BLEを利用しますので、構成ファイル（mbed_app.json）を次のように記述します。また、`"MICROBIT_BLE_DEVICE_INFORMATION_SERVICE=1",`で、Device Information Serviceを有効化しています。

```mbed_app.json
{
    "macros": [
        "MICROBIT_BLE_OPEN=1",
        "MICROBIT_BLE_DFU_SERVICE=0",
        "MICROBIT_BLE_EVENT_SERVICE=0",
        "MICROBIT_BLE_DEVICE_INFORMATION_SERVICE=1",

        "MICROBIT_SD_GATT_TABLE_SIZE=0x340"
    ]
}
```

## アプリの構造 - main.cpp

前回作成したMicroBitIndoorBikeStepSensorクラスと今回作成したMicroBitIndoorBikeStepServiceとを組み合わせます。

```main.cpp
#include "MicroBit.h"
#include "MicroBitIndoorBikeStepSensor.h"
#include "MicroBitIndoorBikeStepService.h"

MicroBit uBit;
MicroBitIndoorBikeStepSensor *sensor;
MicroBitIndoorBikeStepService *service;

void setup()
{
    sensor = new MicroBitIndoorBikeStepSensor(uBit);
    service = new MicroBitIndoorBikeStepService(uBit, *sensor);
    sensor->idleTick();
}

int main()
{
    // Initialise the micro:bit runtime.
    uBit.init();

    // Insert your code here!
    create_fiber(setup);

    // If main exits, there may still be other fibers running or registered event handlers etc.
    // Simply release this fiber, which will mean we enter the scheduler. Worse case, we then
    // sit in the idle task forever, in a power efficient sleep.
    release_fiber();
}
```

## MicroBitIndoorBikeStepServiceクラスの実装

今回、実装したMicroBitIndoorBikeStepServiceクラスについて、その初期化（コンストラクタ）と、FTMSの各特性の実装を順に説明します。

### ケイデンスとスピードへの対応

スピードに関しては、必ずサポートしなければならない為、フラグでのサポート指定は不要です。ケイデンスへの対応をする為、次のようにビットフラグを立てています。

```MicroBitIndoorBikeStepService.h
:
:
/*
# Bit Definitions for the Indoor Bike Data Characteristic
#                                          000 (bits 13-15) Reserved for Future Use
#                                             0 (bit 12) Remaining Time Present
#                                              0 (bit 11) Elapsed Time Present
#                                               0 (bit 10) Metabolic Equivalent Present
#                                                0 (bit  9) Heart Rate Present
#                                                 0 (bit  8) Expended Energy Present
#                                                  0 (bit  7) Average Power Present
#                                                   0 (bit  6) Instantaneous Power Present
#                                                    0 (bit  5) Resistance Level Present
#                                                     0 (bit  4) Total Distance Present
#                                                      0 (bit  3) Average Cadence present
#                                                       1 (bit  2)*Instantaneous Cadence (uint16, 1/minute with a resolution of 0.5)
#                                                        0 (bit  1) Average Speed present
#                                                         0 (bit  0) More Data
#                                          5432109876543210 */
#define FTMP_FLAGS_INDOOR_BIKE_DATA_CHAR 0b0000000000000100
:
:
/*
# Definition of the bits of the Fitness Machine Features field
#                                                  000000000000000 (bits 17-31) Reserved for Future Use
#                                                                 0 (bit 16) User Data Retention Supported
#                                                                  0 (bit 15) Force on Belt and Power Output Supported
#                                                                   0 (bit 14) Power Measurement Supported
#                                                                    0 (bit 13) Remaining Time Supported
#                                                                     0 (bit 12) Elapsed Time Supported
#                                                                      0 (bit 11) Metabolic Equivalent Supported
#                                                                       0 (bit 10) Heart Rate Measurement Supported
#                                                                        0 (bit  9) Expended Energy Supported
#                                                                         0 (bit  8) Stride Count Supported
#                                                                          0 (bit  7) Resistance Level Supported
#                                                                           0 (bit  6) Step Count Supported
#                                                                            0 (bit  5) Pace Supported
#                                                                             0 (bit  4) Elevation Gain Supported
#                                                                              0 (bit  3) Inclination Supported
#                                                                               0 (bit  2) Total Distance Supported
#                                                                                1 (bit  1)*Cadence Supported
#                                                                                 0 (bit  0) Average Speed Supported
#                                                  10987654321098765432109876543210 */
#define FTMP_FLAGS_FITNESS_MACINE_FEATURES_FIELD 0b00000000000000000000000000000010

/*
# Definition of the bits of the Target Setting Features field
#                                                  000000000000000 (bits 17-31) Reserved for Future Use
#                                                                 0 (bit 16) Targeted Cadence Configuration Supported
#                                                                  0 (bit 15) Spin Down Control Supported
#                                                                   0 (bit 14) Wheel Circumference Configuration Supported
#                                                                    0 (bit 13) Indoor Bike Simulation Parameters Supported
#                                                                     0 (bit 12) Targeted Time in Five Heart Rate Zones Configuration Supported
#                                                                      0 (bit 11) Targeted Time in Three Heart Rate Zones Configuration Supported
#                                                                       0 (bit 10) Targeted Time in Two Heart Rate Zones Configuration Supported
#                                                                        0 (bit  9) Targeted Training Time Configuration Supported
#                                                                         0 (bit  8) Targeted Distance Configuration Supported
#                                                                          0 (bit  7) Targeted Stride Number Configuration Supported
#                                                                           0 (bit  6) Targeted Step Number Configuration Supported
#                                                                            0 (bit  5) Targeted Expended Energy Configuration Supported
#                                                                             0 (bit  4) Heart Rate Target Setting Supported
#                                                                              0 (bit  3) Power Target Setting Supported
#                                                                               0 (bit  2) Resistance Target Setting Supported
#                                                                                0 (bit  1) Inclination Target Setting Supported
#                                                                                 0 (bit  0) Speed Target Setting Supported
#                                                  10987654321098765432109876543210 */
#define FTMP_FLAGS_TARGET_SETTING_FEATURES_FIELD 0b00000000000000000000000000000000
:
:
```

### Advertising Data の書き換えと追加

コンストラクタの初めで、`Advertising Data`関係の`Appearance`と`COMPLETE_LOCAL_NAME`とを書き換え、`FTMS`に関する`Advertising Data`を登録しています。

```MicroBitIndoorBikeStepService.cpp
MicroBitIndoorBikeStepService::MicroBitIndoorBikeStepService(MicroBit &_uBit, MicroBitIndoorBikeStepSensor &_indoorBike, uint16_t id)
    : uBit(_uBit), indoorBike(_indoorBike)
{
    :
    :
    // BLE Appearance and LOCAL_NAME
    uBit.ble->gap().accumulateAdvertisingPayload(GapAdvertisingData::GENERIC_CYCLING);
    if (BLE_DEVICE_LOCAL_NAME_CHENGE)
    {
        uBit.ble->gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LOCAL_NAME
            , (const uint8_t *)BLE_DEVICE_LOCAL_NAME, sizeof(BLE_DEVICE_LOCAL_NAME)-1);
    }
    
    // FTMS - Service Advertising Data
    const uint8_t FTMS_UUID[sizeof(UUID::ShortUUIDBytes_t)] = {0x26, 0x18};
    uBit.ble->accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LIST_16BIT_SERVICE_IDS, FTMS_UUID, sizeof(FTMS_UUID));
    uint8_t serviceData[2+1+2];
    struct_pack(serviceData, "<HBH", 0x1826, 0x01, 1<<5);
    uBit.ble->accumulateAdvertisingPayload(GapAdvertisingData::SERVICE_DATA, serviceData, sizeof(serviceData));
    :
    :

}

```

### FTMSサービスの構造と登録

全ての特性(Characteristic)のUUIDや送受信データサイズ、プロパティ等は、FTMP/FTMSの仕様書等で定義されていますので、それに従って、FTMSの特性（`GattCharacteristic`）を定義します。
定義した特性のリストを引数に指定したサービス（`GattService`）を定義し、`uBit.ble->addService(service);`で登録します。
サービスの登録後、各特性のハンドルをメンバー変数に保持します。

```MicroBitIndoorBikeStepService.cpp
MicroBitIndoorBikeStepService::MicroBitIndoorBikeStepService(MicroBit &_uBit, MicroBitIndoorBikeStepSensor &_indoorBike, uint16_t id)
    : uBit(_uBit), indoorBike(_indoorBike)
{
    :
    :
    // Caractieristic
    GattCharacteristic  indoorBikeDataCharacteristic(
        UUID(0x2AD2)
        , (uint8_t *)&indoorBikeDataCharacteristicBuffer, 0, indoorBikeDataCharacteristicBufferSize
        , GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY
    );
    GattCharacteristic  fitnessMachineControlPointCharacteristic(
        UUID(0x2AD9)
        , (uint8_t *)&fitnessMachineControlPointCharacteristicBuffer, 0, fitnessMachineControlPointCharacteristicBufferSize
        , GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_WRITE|GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_INDICATE
    );
    GattCharacteristic  fitnessMachineFeatureCharacteristic(
        UUID(0x2ACC)
        , (uint8_t *)&fitnessMachineFeatureCharacteristicBuffer, 0, fitnessMachineFeatureCharacteristicBufferSize
        , GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_READ
    );
    GattCharacteristic  fitnessMachineStatusCharacteristic(
        UUID(0x2ADA)
        , (uint8_t *)&fitnessMachineStatusCharacteristicBuffer, 0, fitnessMachineStatusCharacteristicBufferSize
        , GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY
    );
    GattCharacteristic  fitnessTrainingStatusCharacteristic(
        UUID(0x2AD3)
        , (uint8_t *)&fitnessTrainingStatusCharacteristicBuffer, 0, fitnessTrainingStatusCharacteristicBufferSize
        , GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_READ | GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY
    );
    
    // Set default security requirements
    indoorBikeDataCharacteristic.requireSecurity(SecurityManager::MICROBIT_BLE_SECURITY_LEVEL);
    fitnessMachineControlPointCharacteristic.requireSecurity(SecurityManager::MICROBIT_BLE_SECURITY_LEVEL);
    fitnessMachineFeatureCharacteristic.requireSecurity(SecurityManager::MICROBIT_BLE_SECURITY_LEVEL);
    fitnessMachineStatusCharacteristic.requireSecurity(SecurityManager::MICROBIT_BLE_SECURITY_LEVEL);
    fitnessTrainingStatusCharacteristic.requireSecurity(SecurityManager::MICROBIT_BLE_SECURITY_LEVEL);

    // Service
    GattCharacteristic *characteristics[] = {
        &indoorBikeDataCharacteristic,
        &fitnessMachineControlPointCharacteristic,
        &fitnessMachineFeatureCharacteristic,
        &fitnessMachineStatusCharacteristic,
        &fitnessTrainingStatusCharacteristic,
    };
    GattService service(
        UUID(0x1826), characteristics, sizeof(characteristics) / sizeof(GattCharacteristic *)
    );
    uBit.ble->addService(service);
    
    // Characteristic Handle
    indoorBikeDataCharacteristicHandle = indoorBikeDataCharacteristic.getValueHandle();
    fitnessMachineControlPointCharacteristicHandle = fitnessMachineControlPointCharacteristic.getValueHandle();
    fitnessMachineFeatureCharacteristicHandle = fitnessMachineFeatureCharacteristic.getValueHandle();
    fitnessMachineStatusCharacteristicHandle = fitnessMachineStatusCharacteristic.getValueHandle();
    fitnessTrainingStatusCharacteristicHandle = fitnessTrainingStatusCharacteristic.getValueHandle();
    :
    :
}

```

### 読み取りプロパティを含む特性の初期値設定

サーバーからの読み取りプロパティ（`GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_READ`）を含む特性の初期値をコンストラクタで、設定しています。
`struct_pack()`は、ヘルパー関数です。

```MicroBitIndoorBikeStepService.cpp
MicroBitIndoorBikeStepService::MicroBitIndoorBikeStepService(MicroBit &_uBit, MicroBitIndoorBikeStepSensor &_indoorBike, uint16_t id)
    : uBit(_uBit), indoorBike(_indoorBike)
{
    :
    :
    // GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_READ
    uint8_t fitnessMachineFeatureBuff[fitnessMachineFeatureCharacteristicBufferSize];
    struct_pack(fitnessMachineFeatureBuff
        , "<II"
        , FTMP_FLAGS_FITNESS_MACINE_FEATURES_FIELD
        , FTMP_FLAGS_TARGET_SETTING_FEATURES_FIELD
    );
    uBit.ble->gattServer().write(fitnessMachineFeatureCharacteristicHandle
        ,(uint8_t *)&fitnessMachineFeatureBuff, fitnessMachineFeatureCharacteristicBufferSize);
    uint8_t fitnessTrainingStatusBuff[fitnessTrainingStatusCharacteristicBufferSize];
    struct_pack(fitnessTrainingStatusBuff
        , "<BB"
        , FTMP_FLAGS_TRAINING_STATUS_FIELD_00_STATUS_ONLY
        , FTMP_VAL_TRAINING_STATUS_01_IDEL
    );
    uBit.ble->gattServer().write(fitnessTrainingStatusCharacteristicHandle
        ,(uint8_t *)&fitnessTrainingStatusBuff, fitnessTrainingStatusCharacteristicBufferSize);
    :
    :
}
```

### イベントハンドラの設定

コンストラクタの最後でイベントハンドラを登録しています。

`Fitness Machine Control Point`特性は、サーバーからの書き込みプロパティ(`GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_WRITE`)を含む特性ですので、サーバーから書き込みがあると、このインスタンスの`MicroBitIndoorBikeStepService::onDataWritten`関数が呼び出されます。

`MicroBitIndoorBikeStepSensor`クラスのインスタンスと連携する為、そのインスタンスの`Event Bus ID`と`MICROBIT_INDOOR_BIKE_STEP_SENSOR_EVT_DATA_UPDATE`イベントを登録しています。
また、`Fitness Machine Control Point`特性への書き込みに対して、検証済みの場合、独自の`MicroBitEvent`を生成していますので（後述）、そのハンドラ（`MicroBitIndoorBikeStepService::onFitnessMachineControlPoint`）を定義しています。

```MicroBitIndoorBikeStepService.cpp
MicroBitIndoorBikeStepService::MicroBitIndoorBikeStepService(MicroBit &_uBit, MicroBitIndoorBikeStepSensor &_indoorBike, uint16_t id)
    : uBit(_uBit), indoorBike(_indoorBike)
{
    :
    :
    // GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_WRITE - Fitness Machine Control Point Characteristic
    uBit.ble->onDataWritten(this, &MicroBitIndoorBikeStepService::onDataWritten);
    
    // Microbit Event listen
    if (EventModel::defaultEventBus)
    {
        EventModel::defaultEventBus->listen(this->indoorBike.getId(), MICROBIT_INDOOR_BIKE_STEP_SENSOR_EVT_DATA_UPDATE
            , this, &MicroBitIndoorBikeStepService::indoorBikeUpdate, MESSAGE_BUS_LISTENER_IMMEDIATE);
        EventModel::defaultEventBus->listen(this->id, MICROBIT_EVT_ANY
            , this, &MicroBitIndoorBikeStepService::onFitnessMachineControlPoint, MESSAGE_BUS_LISTENER_IMMEDIATE);
    }

}
```

### Indoor Bike Data の通知（notify）

`MicroBitIndoorBikeStepSensor`クラスのインスタンスからのイベント（`MicroBitEvent`）を受けて、スピードとケイデンスを`indoorBikeDataCharacteristicHandle`ハンドルに`notify()`関数で書き込んでいます。
実際には、この実装だけで、スマホ・アプリのBLEツールでスピードとケイデンスを表示することが可能です。

```MicroBitIndoorBikeStepService.cpp
void MicroBitIndoorBikeStepService::indoorBikeUpdate(MicroBitEvent e)
{
    if (uBit.ble->getGapState().connected)
    {
        uint8_t buff[indoorBikeDataCharacteristicBufferSize];
        struct_pack(buff, "<HHH",
            FTMP_FLAGS_INDOOR_BIKE_DATA_CHAR,
            this->indoorBike.getSpeed100(),
            this->indoorBike.getCadence2()
        );
        uBit.ble->gattServer().notify(indoorBikeDataCharacteristicHandle
            , (uint8_t *)&buff, indoorBikeDataCharacteristicBufferSize);
    }
}

```

### Fitness Machine Control Point特性への書き込み時の振る舞い

`Fitness Machine Control Point`特性への書き込みがあると、`MicroBitIndoorBikeStepService::onDataWritten`関数が呼び出されますので、ここで、`Op Code`の分類とデータ検証を行い、`Op Code`の`0x80(Response)`で応答しています。データ検証済み（`FTMP_RESULT_CODE_CPPR_01_SUCCESS`）であれば、独自イベントを生成し、結果的に`MicroBitIndoorBikeStepService::onFitnessMachineControlPoint`関数が呼び出されます。

尚、本関数の末尾で、デバッグ用にUSBシリアル通信で、書き込まれたデータを出力しています。

```MicroBitIndoorBikeStepService.cpp
void MicroBitIndoorBikeStepService::onDataWritten(const GattWriteCallbackParams *params)
{
    if (params->handle == fitnessMachineControlPointCharacteristicHandle && params->len >= 1)
    {
        uint8_t responseBuffer[3];
        responseBuffer[0] = FTMP_OP_CODE_CPPR_80_RESPONSE_CODE;
        uint8_t *opCode=&responseBuffer[1];
        opCode[0]=params->data[0];
        uint8_t *result=&responseBuffer[2];
        result[0] = FTMP_RESULT_CODE_CPPR_03_INVALID_PARAMETER;
        uint16_t eventValue = MICROBIT_EVT_ANY;
        switch (opCode[0])
        {
        case FTMP_OP_CODE_CPPR_00_REQUEST_CONTROL:
            eventValue=FTMP_EVENT_VAL_OP_CODE_CPPR_00_REQUEST_CONTROL;
            if (params->len == 1)
            {
                result[0] = FTMP_RESULT_CODE_CPPR_01_SUCCESS;
            }
            break;

        case FTMP_OP_CODE_CPPR_01_RESET:
            eventValue=FTMP_EVENT_VAL_OP_CODE_CPPR_01_RESET;
            if (params->len == 1)
            {
                result[0] = FTMP_RESULT_CODE_CPPR_01_SUCCESS;
            }
            break;

        case FTMP_OP_CODE_CPPR_07_START_RESUME:
            eventValue=FTMP_EVENT_VAL_OP_CODE_CPPR_07_START_RESUME;
            if (params->len == 1)
            {
                result[0] = FTMP_RESULT_CODE_CPPR_01_SUCCESS;
            }
            break;

        case FTMP_OP_CODE_CPPR_08_STOP_PAUSE:
            eventValue=FTMP_EVENT_VAL_OP_CODE_CPPR_08_STOP_PAUSE;
            if (params->len == 2)
            {
                this->stopOrPause = params->data[1];
                result[0] = FTMP_RESULT_CODE_CPPR_01_SUCCESS;
            }
            break;
            
        default:
            result[0] = FTMP_RESULT_CODE_CPPR_02_NOT_SUPORTED;
            break;

        }

        // Response - Fitness Machine Control Point
        uBit.ble->gattServer().write(fitnessMachineControlPointCharacteristicHandle
                , (const uint8_t *)&responseBuffer, sizeof(responseBuffer));
        
        // Fire MicroBit Event
        if (result[0]==FTMP_RESULT_CODE_CPPR_01_SUCCESS)
        {
            new MicroBitEvent(this->id, eventValue);
        }

        // Debug - USB Serial
        uBit.serial.printf("CP:%" PRIu32 ", eventValue[%d], opCode[0x%02X], result[0x%02X], data", (uint32_t)system_timer_current_time(), eventValue, opCode[0], result[0]);
        for (int i=0; i<params->len; i++)
        {
            uBit.serial.printf(", 0x%02X", params->data[i]);
        }
        uBit.serial.printf("\r\n");

    }
}

```

### Op Code別の振る舞い

`Fitness Machine Control Point`特性への書き込みがあると、最終的に`MicroBitIndoorBikeStepService::onFitnessMachineControlPoint`が呼び出されますので、この中で、`Op Code`に従って、それぞれの振る舞いを実装しています。

```MicroBitIndoorBikeStepService.cpp
void MicroBitIndoorBikeStepService::sendTrainingStatusIdle(void)
{
    static const uint8_t buff[]={FTMP_FLAGS_TRAINING_STATUS_FIELD_00_STATUS_ONLY, FTMP_VAL_TRAINING_STATUS_01_IDEL};
    uBit.ble->gattServer().notify(this->fitnessTrainingStatusCharacteristicHandle
        , (uint8_t *)&buff, sizeof(buff));
}

void MicroBitIndoorBikeStepService::sendTrainingStatusManualMode(void)
{
    static const uint8_t buff[]={FTMP_FLAGS_TRAINING_STATUS_FIELD_00_STATUS_ONLY, FTMP_VAL_TRAINING_STATUS_0D_MANUAL_MODE};
    uBit.ble->gattServer().notify(this->fitnessTrainingStatusCharacteristicHandle
        , (uint8_t *)&buff, sizeof(buff));
}
    
void MicroBitIndoorBikeStepService::sendFitnessMachineStatusReset(void)
{
    static const uint8_t buff[]={FTMP_OP_CODE_FITNESS_MACHINE_STATUS_01_RESET};
    uBit.ble->gattServer().notify(this->fitnessMachineStatusCharacteristicHandle
        , (uint8_t *)&buff, sizeof(buff));
}

void MicroBitIndoorBikeStepService::onFitnessMachineControlPoint(MicroBitEvent e)
{
    switch (e.value)
    {
    case FTMP_EVENT_VAL_OP_CODE_CPPR_00_REQUEST_CONTROL:
        // # 0x00 M Request Control
        // #define FTMP_EVENT_VAL_OP_CODE_CPPR_00_REQUEST_CONTROL
        // (NOP)
        break;
    case FTMP_EVENT_VAL_OP_CODE_CPPR_01_RESET:
        // # 0x01 M Reset
        // #define FTMP_EVENT_VAL_OP_CODE_CPPR_01_RESET
        this->sendTrainingStatusManualMode();
        break;
    case FTMP_EVENT_VAL_OP_CODE_CPPR_07_START_RESUME:
        // # 0x07 M Start or Resume
        // #define FTMP_EVENT_VAL_OP_CODE_CPPR_07_START_RESUME
        this->sendTrainingStatusManualMode();
        break;
    case FTMP_EVENT_VAL_OP_CODE_CPPR_08_STOP_PAUSE:
        // # 0x08 M Stop or Pause [UINT8, 0x01-STOP, 0x02-PAUSE]
        // #define FTMP_EVENT_VAL_OP_CODE_CPPR_08_STOP_PAUSE
        this->sendTrainingStatusIdle();
        break;
    default:
        break;
    }
    
}
```

# スマホ・アプリからの接続

FTMSの実装ができているかどうかを確認する為、まずは、BLEツールを使って接続します。
さらに、バーチャルライドアプリにも接続してみます。

## BLEツール - nRF Connect for Mobile

BLEツールとして、Android用のnRF Connect for Mobileを使用します。
https://play.google.com/store/apps/details?id=no.nordicsemi.android.mcp

アプリを起動すると、`STEP:BIT`というデバイスが表示されますので、`Connect`で接続します。
接続に成功すると`STEP:BIT`タブに切り替わりますので、`Fitness Machine`をタップし、`Indoor Bike Data`の右横のアイコンをタップして、`NOTIFY`を受け入れるようにします。ペダリング（ワイヤレス運動測量計）に連動して、`Instantaneous Speed`と`Instantaneous Cadence`が表示されます。表示の単位は、`km/h`や`per min`であり、FTMSサービス上での単位をツールが変換して表示しています。

## バーチャルライドアプリ - OneLap

[OneLap - https://www.onelap.com/](https://www.onelap.com/)
[OneLap Japan - https://www.onelapjapan.jp/](https://www.onelapjapan.jp/)

各社からバーチャルライドアプリ（サービス）が提供されており、ZwiftやKinomap、RGT Cyclingなどを試しています。その中でも、中国発のOneLapというサービスが日本の代理店を通じて、サービスが提供されています。
2021年2月現在、3か月間の無償クーポンが発行されていますので、今が、お試しのチャンスです。また、有償プランとしては、月額費用も他のサービスと比較するとかなりリーズナブルな設定です。

OneLapを起動し、Deviceを選択します。各社の室内トレーナーから選択できますが、OtherのFTMSを選択します。また、パワーとスピードを選べますが、FTMSで、パワーの実装をしていませんので、スピードの方を選んでください。

ところが、実際に、ペダリングしてみると分かりますが、スピードの値が、ワイヤレス運動測量計の値よりもかなり速い値で表示されます。

## バーチャルライドアプリのスピード補正

スピードを補正する必要がありそうですので、まずは、常に100.00km/hになるようにスピードの値を固定します(`*speed100 = 10000;`)。

```MicroBitIndoorBikeStepSensor.cpp
void calcIndoorBikeData(uint32_t crankIntervalTime, uint32_t* cadence2, uint32_t* speed100)
{
    
    if (crankIntervalTime==0)
    {
        *cadence2 = 0;
        *speed100 = 0;
    }
    else
    {
        *cadence2 = (uint32_t)( (uint64_t)K_STEP_CADENCE / crankIntervalTime );
        *speed100 = 10000; //(uint32_t)( (uint64_t)K_STEP_SPEED   / crankIntervalTime );
    }
}
```

すると、OneLap側では、738km/hと表示されますので、7.38倍の速さで表示されていることが分かります。
ただし、走行モードになると、感覚としては、5倍程度の速さで表示されているようです。
ですので、ケイデンスから算出したスピードを5から7で割ったスピードの補正値を返すようにすれば良いようです。

> *speed100 = (uint32_t)( (uint64_t)K_STEP_SPEED   / crankIntervalTime / 5);

![Screenshot_2021-02-15-08-54-01-645_com.magene.onelapUS.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252891/44cf1fc1-f631-de52-c945-31e29a31456c.jpeg)

# おわりに

・FTMP/FTMSの仕様書を斜め読みしました
・ケイデンスとスピードに対応したFTMSの実装をしました
・スマホ・アプリから接続し、ケイデンスとスピードを確認しました
・バーチャルライドアプリでのスピードはその値の補正が必要であることがわかりました

[次回（その５）](https://qiita.com/jp-96/items/f7432824716f55de4930)は、スピードからパワーを推定し、FTMSとしてパワーを利用できるように改良します。
また、一定時間、バーチャルライドアプリと接続していると、Micro:BitがPanic状態もしくは無応答状態になってしまうので、そのあたりの改善も行います。

<!--
リンクを別タブで開く
https://qiita.com/yukichi/items/df4f5126f193a1c8efdb
-->
<script>
jQuery(document).ready(function(){
    jQuery('a[href^=http]')
    .not('[href*="'+location.hostname+'"]')
    .attr({target:"_blank"})
    .addClass("external")
    ;})  
</script>