家庭用サイクルトレーナーにmicro:bitをつなぐ（その３）- STEP信号からケイデンスとスピードを求めるモジュールをドライバー（アイドル・コンポーネント）として作成する

# 家庭用サイクルトレーナーにmicro:bitをつなぐ（その３）
前回までの記事（[その１](https://qiita.com/jp-96/items/110b6364a3da89553b27)、[その２](https://qiita.com/jp-96/items/c494da04f04603ef258d)）では、家庭用サイクルトレーナーのワイヤレス運動測量計とmicro:bitをつなぎ、STEP信号からケイデンスとスピードを算出・推定することができるようになりました。

**リンク**

1. [家庭用サイクルトレーナーにmicro:bitをつなぐ（その１）  
ワイヤレス運動測量計から出力されるクランク回転センサーの信号をモニターする](https://qiita.com/jp-96/items/110b6364a3da89553b27)
2. [家庭用サイクルトレーナーにmicro:bitをつなぐ（その２）  
STEP信号からワイヤレス運動測量計に表示されているスピードをmicro:bitで推定する](https://qiita.com/jp-96/items/c494da04f04603ef258d)

今回、`MicroBitComponent`を継承し、ワイヤレス運動測量計のSTEP信号からケイデンスとスピードを求めるモジュールをドライバー化した**`MicroBitIndoorBikeStepSensor`クラス**を作成しました。

![IMG_20210211_082808.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252891/2cca8f69-263a-a25f-4269-2a6118393d68.jpeg)

# `MicroBitIndoorBikeStepSensor`クラスの使用例

`MicroBitIndoorBikeStepSensor`クラスは、`MicroBitComponent`を継承したドライバーです。次の`main.cpp`で、その使い方の実装例を示しています。

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

MicroBit uBit;
MicroBitIndoorBikeStepSensor stepSensor(uBit);

uint32_t stepCount;

void onStepSensor(MicroBitEvent e)
{
    uBit.serial.printf("%" PRIu32 ", %" PRIu32 ", %" PRIu32 ", %" PRIu32 "\r\n"
        , ++stepCount, (uint32_t)e.timestamp, stepSensor.getCadence2() , stepSensor.getSpeed100());
}

void setup(void)
{    
    uBit.serial.printf("#, Timestamp, Cadence2, Speed100\r\n");
    uBit.addIdleComponent(&stepSensor);
    uBit.messageBus.listen(MICROBIT_INDOORBIKE_STEP_SENSOR_ID, MICROBIT_INDOOR_BIKE_STEP_SENSOR_EVT_DATA_UPDATE, onStepSensor);
    uBit.display.scrollAsync("GO!");
}

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();
}

```

## 説明
**`MicroBitIndoorBikeStepSensor`クラス**のインスタンス変数`stepSensor`を定義し、`stepSensor`を`uBit.addIdleComponent`メソッドで、`uBit`インスタンスに追加します。
`MicroBitIndoorBikeStepSensor`クラスのインスタンスには、イベント・バスIDに**`MICROBIT_INDOORBIKE_STEP_SENSOR_ID`**がデフォルトで割り当ててありますので、`uBit.messageBus.listen`メソッドで、`MicroBitIndoorBikeStepSensor`インスタンスが発生させる**`MICROBIT_INDOOR_BIKE_STEP_SENSOR_EVT_DATA_UPDATE`イベント**を`onStepSensor`イベントハンドラで受け取るようにします。
`onStepSensor`イベントハンドラでは、**ケイデンス(単位:0.5rpm)**と**スピード(単位:0.01km/h)**を`MicroBitIndoorBikeStepSensor`インスタンスから取得し、USBシリアル通信で、出力しています。

|項目|内容|備考|
|---|---|---|
|クラス|MicroBitIndoorBikeStepSensor|MicroBitComponentを継承（IdleComponent）|
|イベント・バスID|MICROBIT_INDOORBIKE_STEP_SENSOR_ID|ユーザー定義|
|値更新イベント|MICROBIT_INDOOR_BIKE_STEP_SENSOR_EVT_DATA_UPDATE|約1秒間隔で発生|
|接続端子|端子P2|デフォルト|

## 取得値の単位（精度）について

取得できるケイデンスとスピードの単位（精度）の仕様は少し特殊ですので注意してください。**`MicroBitIndoorBikeStepSensor::getCadence2()`メソッド**で取得できる値1は、**0.5rpm**としており、**`MicroBitIndoorBikeStepSensor::getSpeed100()`メソッド**で取得できる値1は、**0.01km/h**としています。
例えば、`getCadence2()`の取得値が`140`の場合は、`140×0.5rpm=70.0rpm`であり、`getSpeed100()`の取得値が、`2345`の場合は、`2345×0.01km/h=23.45km/h`です。

|項目|メソッド|単位（精度）|例|
|---|---|---|---|
|ケイデンス|getCadence2|0.5rpm|取得値140の場合、140×0.5rpm=70.0rpm|
|スピード|getSpeed100|0.01km/h|取得値2345の場合、2345×0.01km/h=23.45km/h|

#  `MicroBitIndoorBikeStepSensor`クラスの実装（説明）

## 実際の継承元

`MicroBitIndoorBikeStepSensor`クラスは、`MicroBitComponent`を継承していると説明しましたが、実際には、`MicroBitComponent`を継承した`MicroBitCustomComponent`を継承しています。

```MicroBitCustomComponent.h
#include "MicroBit.h"

// The base of custom Event Bus ID.
static const uint16_t MICROBIT_CUSTOM_ID_BASE = 32768;

class MicroBitCustomComponent : public MicroBitComponent
{
public:

    /**
      * Event Bus ID of this component.
      */ 
    uint16_t getId(void)
    {
        return this->id;
    }

};
```

理由は２つあります。

1. イベント・バスIDを取得したい為
1. イベント・バスIDのユーザー定義値において、標準の定義値との重複を避けたい為

**1つ目の理由**は、今回の使用例では、使用していませんが、そのインスタンス変数から保持しているイベント・バスIDを取得したい場合があるためです。例えば、インスタンス変数を定義する際に、コンストラクタへ動的にイベント・バスIDを割り当てるような実装をしたい場合が考えられます。その場合は、`uBit.messageBus.listen`メソッドの第１引数には、そのインスタンスのイベント・バスIDを取得する為に、`stepSensor.getId()`を与えます。

**2つ目の理由**は、継承とは関係ありませんが、ユーザー定義されるイベント・バスIDの範囲を標準化しています(`32768`以上の値)。

## ケイデンスとスピードの計算

前回までの記事で、ケイデンスとスピードの計算式を求めましたが、それらの計算式を使った`calcIndoorBikeData`メソッドで算出しています。
STEP信号の間隔(単位:マイクロ秒) `crankIntervalTime` から、ケイデンス(単位:0.5rpm)`cadence2`とスピード(単位:0.01km/h)`speed100`に結果を返します。

``` 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 = (uint32_t)( (uint64_t)K_STEP_SPEED   / crankIntervalTime );
    }
}
```

## コンストラクタ

コンストラクタでは、主に次の３つを実装しています。

1. メンバ変数の初期化
2. STEP信号の取得イベントハンドラの設定
3. エッジイベントの有効化

```MicroBitIndoorBikeStepSensor.cpp
MicroBitIndoorBikeStepSensor::MicroBitIndoorBikeStepSensor(MicroBit &_uBit, MicrobitIndoorBikeStepSensorPin pin, uint16_t id)
    : uBit(_uBit)
{
    this->id = id;
    this->lastIntervalTime=0;
    this->lastCadence2=0;
    this->lastSpeed100=0;
    this->updateSampleTimestamp=0;
    
    if (EventModel::defaultEventBus)
        EventModel::defaultEventBus->listen(MICROBIT_INDOOR_BIKE_STEP_SENSOR_EVENT_IDs[pin], MICROBIT_PIN_EVT_FALL
            , this, &MicroBitIndoorBikeStepSensor::onStepSensor);
    
    switch (pin)
    {
    case EDGE_P0:
        uBit.io.P0.eventOn(MICROBIT_PIN_EVENT_ON_EDGE);
        break;
    case EDGE_P1:
        uBit.io.P1.eventOn(MICROBIT_PIN_EVENT_ON_EDGE);
        break;
    default:    // EDGE_P2
        uBit.io.P2.eventOn(MICROBIT_PIN_EVENT_ON_EDGE);
        break;
    }
    
}
```

## STEP信号の取得イベントハンドラ

ドライバー内では、`MICROBIT_PIN_EVT_FALL`イベントが発生した`e.timestamp`値をリスト`intervalList`に保持しています（push,pop）。保持する値は、最新の３つ（`INTERVAL_LIST_SIZE`）です。つまり、最大、2回転分のSTEP信号の平均値からケイデンスやスピードを算出できます。
また、リスト`intervalList`が空の場合、初回から、ケイデンスとスピードを算出できるようにする為、`MAX_STEPS_INTERVAL_TIME_US`マイクロ秒を引いた値を１つ余分に予め追加しています。`MAX_STEPS_INTERVAL_TIME_US`マイクロ秒（2.5秒）の値は、**ワイヤレス運動測量計**が**STOP状態**と判断する最大値を実験的に求めた値です。

```MicroBitIndoorBikeStepSensor.cpp
void MicroBitIndoorBikeStepSensor::onStepSensor(MicroBitEvent e) 
{
    uint64_t currentTime = e.timestamp;
    
    if (this->intervalList.size()==0)
    {
        // 初回から、ケイデンスとスピードを算出する。
        this->intervalList.push(currentTime-MAX_STEPS_INTERVAL_TIME_US);
    }
    this->intervalList.push(currentTime);
    if ((uint32_t)this->intervalList.size()>(this->INTERVAL_LIST_SIZE))
    {
        this->intervalList.pop();
    }
}
```

## 算出とイベント生成 - アイドル・コンポーネントとシステム・コンポーネント

`MicroBitComponent`派生クラスのインスタンスは、**アイドル・コンポーネント**や**システム・コンポーネント**として、MicroBitランタイムに登録されることにより、それぞれの方式で振る舞います（機能します）。

### アイドル・コンポーネント
`uBit.addIdleComponent`メソッドなどでMicroBitランタイムに登録すると、アイドルスレッドから`idleTick`メソッドが頻繁に呼び出されます。

例えば、次の標準コンポーネントがアイドル・コンポーネントとして利用できます。

1. MicroBitBLEManager
1. MicroBitEventService
1. MicroBitIOPinService
1. MicroBitAccelerometer
1. MicroBitCompass
1. MicroBitThermometer.

### システム・コンポーネント
`uBit.addSystemComponent`メソッドなどでMicroBitランタイムに登録すると、タイマー割り込みから`systemTick`メソッドがTick間隔で呼び出されます。尚、タイマー・コンテキストからの呼び出しであることに注意してください。

例えば、次の標準コンポーネントがシステム・コンポーネントとして利用できます。

1. MicroBitSystemTimerCallback
1. MicroBitButton
1. MicroBitDisplay

### ケイデンスとスピードの算出とイベント生成

`MicroBitIndoorBikeStepSensor`クラスは、**アイドル・コンポーネント**として実装してあります。

```MicroBitIndoorBikeStepSensor.cpp
void MicroBitIndoorBikeStepSensor::idleTick()
{
    this->update();
}

```

そして、`update`メソッドでは、`SENSOR_UPDATE_PERIOD_US`マイクロ秒間隔（1秒間隔）で、ケイデンスとスピードを算出し、`MICROBIT_INDOOR_BIKE_STEP_SENSOR_EVT_DATA_UPDATE`イベントを発生させています。
また、`MAX_STEPS_INTERVAL_TIME_US`マイクロ秒（2.5秒）以上、STEP信号の検出がない場合は、停止状態と判断し、ケイデンスとスピードがゼロになるようにしています。

```MicroBitIndoorBikeStepSensor.cpp
void MicroBitIndoorBikeStepSensor::update(void)
{
    uint64_t currentTime = system_timer_current_time_us();

    if(!(status & MICROBIT_INDOOR_BIKE_STEP_SENSOR_ADDED_TO_IDLE))
    {
        // If we're running under a fiber scheduer, register ourselves for a periodic callback to keep our data up to date.
        // Otherwise, we do just do this on demand, when polled through our read() interface.
        fiber_add_idle_component(this);
        status |= MICROBIT_INDOOR_BIKE_STEP_SENSOR_ADDED_TO_IDLE;
    }
    
    if (currentTime >= this->updateSampleTimestamp)
    {
        this->updateSampleTimestamp = currentTime + this->SENSOR_UPDATE_PERIOD_US;
        
        if ((this->intervalList.size()>0) && ((currentTime - this->intervalList.back())>=this->MAX_STEPS_INTERVAL_TIME_US))
        {
            while (this->intervalList.size()>0)
            {
                this->intervalList.pop();
            }
        }
        
        if (this->intervalList.size() < 2)
        {
            this->lastIntervalTime = 0;
        }
        else
        {
            uint64_t intervalNum = this->intervalList.size() - 1;
            uint64_t periodTime = this->intervalList.back() - this->intervalList.front();
            this->lastIntervalTime = periodTime / intervalNum;
        }
        
        calcIndoorBikeData(this->lastIntervalTime, &this->lastCadence2, &this->lastSpeed100);
        
        MicroBitEvent e(id, MICROBIT_INDOOR_BIKE_STEP_SENSOR_EVT_DATA_UPDATE);
    }
}

```

# おわりに

- `MicroBitIndoorBikeStepSensor`クラスを作成しました
- `MicroBitIndoorBikeStepSensor`クラスの使用例を示しました
- `MicroBitIndoorBikeStepSensor`クラスの実装内容について説明しました

[次回（その４）](https://qiita.com/jp-96/items/abb988a286bfb72ea7c6)は、スマホ・アプリでケイデンスやスピードを表示できるように BLEのGATTサービス(FTMP/FTMS)を実装します。


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