📌

『M5Stack Core2 for AWS - ESP32 IoT開発キット』入門 後編 〜Deep Dive〜

2021/05/26に公開

Qiitaにも投稿してます→【2021年4月版】『M5Stack Core2 for AWS - ESP32 IoT開発キット』 入門 後編 〜Getting Started Deep Dive〜

"Getting Started" Deep Dive

前編ではM5Stack Core2の開発環境構築からGetting Startedプログラムを動かしてみて、Core2とスマホアプリRainMakerの挙動について確認いたしました。
それでは今回は、このプロジェクトの実装についてちょいとソースコードの海へ潜り込んでみましょう。

まずはプロジェクトのフォルダ構成です。

% tree -L 2
.
├── CMakeLists.txt
├── LICENSE
├── Makefile
├── README.md
├── cli
│   ├── html
│   ├── rainmaker.py
│   ├── requirements.txt
│   ├── rmaker_cmd
│   ├── rmaker_lib
│   ├── rmaker_tools
│   └── server_cert
├── components
│   ├── app_reset
│   ├── app_wifi
│   ├── button
│   ├── core2forAWS
│   ├── esp-cryptoauthlib
│   ├── esp_rainmaker
│   ├── esp_schedule
│   ├── json_generator
│   ├── json_parser
│   ├── qrcode
│   └── ws2812_led
├── docs
│   ├── Doxyfile
│   ├── Makefile
│   ├── README.md
│   ├── _static
│   ├── c-api-reference
│   ├── conf.py
│   ├── gen-dxd.py
│   ├── index.rst
│   ├── link-roles.py
│   ├── local_util.py
│   ├── make.bat
│   ├── python-api-reference
│   └── requirements.txt
├── main
│   ├── CMakeLists.txt
│   ├── app_driver.c
│   ├── app_main.c
│   ├── app_priv.h
│   ├── component.mk
│   ├── display.c
│   ├── display.h
│   ├── fan.c
│   ├── fan.h
│   ├── fan_1.c
│   ├── fan_2.c
│   ├── fan_3.c
│   ├── fan_4.c
│   ├── fan_5.c
│   ├── fan_6.c
│   ├── fan_off.c
│   ├── house_off.c
│   ├── house_on.c
│   ├── hsv2rgb.c
│   ├── hsv2rgb.h
│   ├── light.c
│   ├── light.h
│   ├── temperature.c
│   ├── temperature.h
│   ├── thermometer.c
│   └── user_parameters.h
├── partitions_4MB_sec.csv
├── platformio.ini
├── sdkconfig
└── sdkconfig.defaults

23 directories, 46 files

メインは main フォルダ内のソースコードで、clicomponentsはベースとなるツールやライブラリ、docsはまぁそのままドキュメント生成プロジェクトであろうかと思います。
今回は main フォルダの中身についてさらに潜っていきたいと思います。

1. app_*.*→"RainMaker アプリ"の実装

app_*.* を主な部分だけざっと貼り付けますと、

app_driver.c
...
esp_err_t app_init(void)
{

    return ESP_OK;
}
app_priv.h
...
#pragma once
app_main.c
...
static const char *TAG = "app_main";

extern SemaphoreHandle_t spi_mutex;

void app_main()
{
    /* Initialize Application specific hardware drivers and
     * set initial state.
     */
    spi_mutex = xSemaphoreCreateMutex();

    Core2ForAWS_Init();
    Core2ForAWS_PMU_Init(3300, 0, 0, 2700);   
    display_init();
    Core2ForAWS_Button_Init();

    /* Initialize NVS. */
    esp_err_t err = nvs_flash_init();
...

    /* Initialize Wi-Fi. Note that, this should be called before esp_rmaker_init()
     */
    app_wifi_init();
    
    /* Initialize the ESP RainMaker Agent.
     * Note that this should be called after app_wifi_init() but before app_wifi_start()
     * */
    esp_rmaker_config_t rainmaker_cfg = {
        .enable_time_sync = false,
    };
    esp_rmaker_node_t *node = esp_rmaker_node_init(&rainmaker_cfg, "ESP RainMaker Device", "Fan");
...

    light_init(node);
    temperature_init(node);
    fan_init(node);

    esp_rmaker_schedule_enable();

    /* Start the ESP RainMaker Agent */
    esp_rmaker_start();

    /* Start the Wi-Fi.
     * If the node is provisioned, it will start connection attempts,
     * else, it will start Wi-Fi provisioning. The function will return
     * after a connection has been successfully established
     */
    err = app_wifi_start(POP_TYPE_RANDOM);
...
}

エラーチェックの箇所や長いコメント、includeなどは割愛させていただきました。ヘッダー2つについては特に特筆することはないと思います。
さて、app_main.cのざっくりの流れとしては、

  1. Core2本体の状態初期化
  2. Wifiの初期化
  3. Flashメモリの初期化
  4. RainMaker Agentのインスタンス生成
  5. "機器"の登録("Fan","Light","Temperature")と初期化
  6. RainMaker Agentの立ち上げ
  7. Wifiの接続

といった感じです。
RainMakerとやりとりするRainMaker Agentを立ち上げております。
このAgentがCore2とスマホアプリの通信を宜しくラッピングしてくれている、ということであります。まさに"RainMakerアプリ"の実装ですね。

2. display.*→タッチディスプレイの制御

まずdisplay.h外側から見たモジュールの構造を俯瞰してみましょう。

display.h
#ifndef DISPLAY_H
#define DISPLAY_H

void display_init();
void display_fan_init();
void display_house_init();
void display_temperature_init(void);

void display_lights_off(void);
void display_lights_on(int h, int s, int v);

void display_fan_speed(int s);
void display_fan_off(void);
void display_fan_on(void);

void display_temperature(float c);

#endif

ここではディスプレイの初期化と表示の切り替え関数を宣言しているようですね。
"Light"のオン/オフで表示が切り替わったり、Fanもオン・オフ、スピードの変化について表示が切り替わるようです。

温度については表示のみ、といった感じでしょうか。

なるほど、大体の構造はわかりました
それではヘッダーで宣言されている流れに沿って、display.cの中身をざっくりとご紹介いたします。

2-1. LCDおよびタッチセンサー

まずはdisplay_init関数ですね。ここではLCDとタッチセンサーそのものの初期化処理を行なっております。

display.c
void display_init()
{
    FT6336U_Init();
    Core2ForAWS_LCD_Init();
    Core2ForAWS_LCD_SetBrightness(100);
}

はい、出ましたFT6336U。これが静電容量式タッチセンサーでございます。これの初期化して使用する準備万端、ということですね。
続いてCore2本体のLCDスクリーンの初期化と明るさ最大にしております。

これが画面全体の初期化処理です。この関数はapp_main.cから呼ばれております。

2-2. "Fan"→箇所の画面要素定義

続きましてdisplay_fan_init関数です。

display.c
void display_fan_init()
{

    xSemaphoreTake(xGuiSemaphore, portMAX_DELAY);

    ESP_LOGI(TAG,"configuring the fan");

    fan_object = lv_img_create(lv_scr_act(), NULL);
    lv_img_set_src(fan_object, &fan_off);
    lv_obj_align(fan_object, lv_scr_act(), LV_ALIGN_IN_TOP_RIGHT, -20, 0);
    ESP_LOGI(TAG,"configured fan_object");

    fan_strength_slider = lv_slider_create(lv_scr_act(), NULL);
    lv_obj_set_width(fan_strength_slider, 8);
    lv_obj_set_height(fan_strength_slider, 80);
    lv_obj_align(fan_strength_slider, fan_object, LV_ALIGN_OUT_RIGHT_MID, 0, 0);
    lv_obj_set_event_cb(fan_strength_slider, strength_slider_event_cb);
    lv_slider_set_value(fan_strength_slider, 0, LV_ANIM_OFF);
    lv_slider_set_range(fan_strength_slider, 0, 5);

    ESP_LOGI(TAG,"configured fan_strength_slider");

    fan_sw1 = lv_switch_create(lv_scr_act(), NULL);
    lv_obj_set_size(fan_sw1, 60, 20);
    lv_obj_align(fan_sw1, fan_object, LV_ALIGN_OUT_BOTTOM_MID, 0, 0);
    lv_obj_set_event_cb(fan_sw1, sw1_event_handler);
    lv_switch_off(fan_sw1, LV_ANIM_OFF);
    ESP_LOGI(TAG,"configured fan_sw1");

    xSemaphoreGive(xGuiSemaphore);

    xTaskCreatePinnedToCore(spin_update, "fan", 4096, NULL, 2, NULL, 1);

    ESP_LOGI(TAG,"fan configured");
}

これですこれ、タッチパネルのプログラミングって感じですよね。
ちょっとイメージしづらいとおもいますので、"Fanの箇所"の画像を貼ります。
Display_Fan.png

左上というかメインのFanが後ほどアニメーションしますが、その右の赤い丸がついた上下のスライダーがファンのスピード、下のスライドスイッチがオン・オフを表しています。

このイメージをしつつ流れを掻い摘んでご紹介しますと、

  1. lv_img_createlv_img_set_src でファン画像の描画
  2. fan_strength_slider = lv_slider_create でスライダーコントロールの生成と描画
  3. lv_obj_set_event_cb(fan_strength_slider, strength_slider_event_cb)
    でスライダコントロールのイベントハンドラ割り当て
  4. fan_sw1 = lv_switch_createでスライドスイッチの生成と描画
  5. lv_obj_set_event_cb(fan_sw1, sw1_event_handler) でスイッチのイベントハンドラ割り当て
  6. xTaskCreatePinnedToCoreでFanの画像書き換え非同期タスクの登録=アニメーションの実行

という感じになるかと思います。このFanのサンプルだけでタッチスクリーンのGUIプログラミングの要素が色々汲み取れてしまいますね。素晴らしいです。というか"Getting Started"にしては詰め込みすぎなんじゃないかと思うくらいです笑

ではさらに、イベントハンドラや非同期タスクの中身について深掘りして参りましょう。

2-2-1. strength_slider_event_cb→スライダーイベント

スライダーに触れたときのイベントハンドラです。(スライダ小さいのでおじさんの指じゃもうタッチもなかなかしんどいのですが)

display.c
tatic void strength_slider_event_cb(lv_obj_t * slider, lv_event_t event)
{

    if(event == LV_EVENT_VALUE_CHANGED) {
        fan_set_speed(lv_slider_get_value(slider));
        fan_speed_report();
    }
}

はい、eventでスライダコントロールに割り当てた範囲の"値"が直接、渡してもらえます。で、Fanのスピード変えて、fan_speed_reportで"RainMaker Agent"を通してAWS IoT/MQTTにFanの速度を通知しています。
地味ですが、イベントハンドラがstatic 関数というのは今でも健在なんですね。。。

2-2-2. sw1_event_handler→ボタンイベント

スイッチボタンに触れたときのイベントです。

display.c
static void sw1_event_handler(lv_obj_t * obj, lv_event_t event)
{
    if(event == LV_EVENT_VALUE_CHANGED) {
        fan_set_power(lv_switch_get_state(obj));
        fan_power_report();
    }
}

はい、こちらもコントロールに割り当てた値が変化したら"Fanの電源"を切り替えて、電源の状態をAgentに通知してもらっております。

2-2-4. spin_update→Fanの回転アニメーション

ここではFan画像の切り替えチェックとアニメーションを行なっています。

static void spin_update(void *priv)
{
    int fan_index = 0;
    int speed_skip=0;
    while(1)
    {
        vTaskDelay(pdMS_TO_TICKS(50));
        if(pdTRUE == xSemaphoreTake(xGuiSemaphore, 0))
        {
            if(g_fan_speed && g_fan_power)
            {
                if(speed_skip == 0)
                {
                    lv_img_set_src(fan_object, fanImages[fan_index++]);
                    if(fan_index >= (sizeof(fanImages)/sizeof(*fanImages))) fan_index = 0;
                    speed_skip = 5 - g_fan_speed;
                }
                else
                {
                    speed_skip --;
                }
            }
            else
            {
                lv_img_set_src(fan_object, &fan_off);
            }
            xSemaphoreGive(xGuiSemaphore);
        }
    }
}

ざっくりと流れを紹介いたしますと、

  1. vTaskDelay(pdMS_TO_TICKS(50));で50tick毎に処理を行なう
  2. xSemaphoreTakeで画面への書き込み処理が他の処理と衝突しないようにチェック
  3. g_fan_speed && g_fan_powerでFanがオフなら"fan_off"画像データを書き込む
  4. "speed_skip"カウンタが0になったら次のインデクスのfan画像データを書き込む
  5. 画像のインデクスが画像数を超えたら0に戻す
  6. Fanの速度に応じて"speed_skip"カウンタを増やす

という感じです。
キモは、スピードが早いほうがカウンタが増えない→スピードが早ければ書き換え回数も早い、というところですね。ちゃんとFanのスピード上げれば画像の更新速度も上がっていき、早くすればスピードに応じたFanのアニメーションが行われるようです。
Fanの回っている感じ、伝わるでしょうか?

Display_Fan_Roll.png

"Getting Started"の割にはガチめの演出がされています。

Fanの箇所だけでも色々なギミックがありましたね。タッチディスプレイですしれっきとしたGUIなんですが、やはりサイズ感からか"機器"の"ギミック"に感じますね。いや〜楽しいです。

2-3. "House"箇所の画面要素定義

さて、Fanだけでも若干お腹いっぱい感がありますがまだまだ続きます。次は"House"の部分です。
さきほどの画面を思い出していただくために、"House"の部分を貼っておきます。

Display_House.png

この箇所の初期設定と初期の描画が以下の処理となっております。

display.c
void display_house_init(void)
{
    xSemaphoreTake(xGuiSemaphore, portMAX_DELAY);
    ESP_LOGI(TAG,"configuring the house");

    light_object = lv_img_create(lv_scr_act(),NULL);
    lv_img_set_src(light_object, &house_off);
    lv_obj_align(light_object,lv_scr_act(),LV_ALIGN_IN_TOP_LEFT,0,0);

    window_object = lv_canvas_create(lv_scr_act(),NULL);
    lv_obj_align(window_object, light_object, LV_ALIGN_CENTER,-CANVAS_WIDTH/2,-20);
    lv_canvas_set_buffer(window_object, window_buffer, CANVAS_WIDTH, CANVAS_HEIGHT, LV_IMG_CF_INDEXED_1BIT);
    lv_canvas_set_palette(window_object,0,LV_COLOR_TRANSP);
    lv_canvas_set_palette(window_object,1,LV_COLOR_RED);
    lv_color_t c;
    c.full = 1;
    lv_canvas_fill_bg(window_object,c,LV_OPA_100);

    lv_obj_move_background(window_object);
    lv_img_set_src(light_object, &house_off);
    lv_obj_align(light_object,lv_scr_act(),LV_ALIGN_IN_TOP_LEFT,0,0);

    xSemaphoreGive(xGuiSemaphore);
    ESP_LOGI(TAG,"house configured");
}

light_object = lv_img_createで画像オブジェクトが生成されて、lv_img_set_src(light_object, &house_off);にて、house_offの画像データが初期表示されます。ここまではなんてことのない処理です。
ここで特筆すべきは次のwindow_object = lv_canvas_createです。house_offの画像とは別にwindow_objectが透明色で用意されています。。。窓の部分だけ、別なのは・・・?!

この効果については後ほど述べたいと思います。

2-4. "Temperature"箇所の画面要素定義

続いて温度計の部分の表示処理についてご紹介いたします。
例によって該当部分の画像を貼ります。

Display_Temperature.png

で、ここの表示処理は以下のようになっております。

display.c
void display_temperature_init(void)
{
    xSemaphoreTake(xGuiSemaphore, portMAX_DELAY);
    ESP_LOGI(TAG,"configuring the temperature");
    temperature_object = lv_img_create(lv_scr_act(),NULL);
    lv_img_set_src(temperature_object, &thermometer);
    lv_obj_align(temperature_object,lv_scr_act(),LV_ALIGN_IN_BOTTOM_RIGHT,0,-10);

    thread_object = lv_canvas_create(lv_scr_act(),NULL);
    lv_obj_align(thread_object, temperature_object, LV_ALIGN_IN_TOP_LEFT,15,0);
    lv_canvas_set_buffer(thread_object, temperature_buffer, THREAD_WIDTH, THREAD_HEIGHT, LV_IMG_CF_INDEXED_1BIT);
    lv_canvas_set_palette(thread_object,0,LV_COLOR_GRAY);
    lv_canvas_set_palette(thread_object,1,LV_COLOR_ORANGE);
    lv_color_t c;
    c.full = 0;
    lv_canvas_fill_bg(thread_object,c,LV_OPA_100);

    lv_obj_move_background(thread_object);

    lv_obj_t *label = lv_label_create(lv_scr_act(), NULL);
    lv_obj_align(label, temperature_object, LV_ALIGN_IN_RIGHT_MID, -75, -15);
    lv_label_set_align(label,LV_LABEL_ALIGN_CENTER);
    lv_label_set_text(label, "Internal\nTemperature");

    temperature_label = lv_label_create(lv_scr_act(),NULL);
    lv_obj_align(temperature_label, label, LV_ALIGN_OUT_BOTTOM_MID, 0, 5);
    lv_obj_set_width(temperature_label,75);
    _lv_obj_set_style_local_ptr(temperature_label, LV_OBJ_PART_MAIN, LV_STYLE_TEXT_FONT, &lv_font_montserrat_16);  /*Set a larger font*/
    lv_label_set_align(temperature_label,LV_LABEL_ALIGN_CENTER);
    lv_label_set_text(temperature_label,"");


    xSemaphoreGive(xGuiSemaphore);
    ESP_LOGI(TAG,"temperature configured");
}

この温度計画面、派手なイベントはないですがGUIでのテキスト表示とテキストの動的な変更のエッセンスが詰まっております。地味だけど重要、な箇所ですね。
詳しい処理内容はよくわかりませんが、ざっくりと以下のような流れでしょうか。

  1. temperature_object = lv_img_createlv_img_set_src(temperature_object, &thermometer);にて温度計画像の配置
  2. thread_object = lv_canvas_createlv_canvas_set_palette(thread_object,0,LV_COLOR_GRAY);lv_canvas_set_palette(thread_object,1,LV_COLOR_ORANGE);で温度計のカラーバーを設置
  3. lv_label_set_text(label, "Internal\nTemperature");で固定テキストの設置
  4. temperature_label = lv_label_create(lv_scr_act(),NULL);で温度の表示用テキストエリアを定義

ということで、温度計のバーの部分と温度のテキストは動的に更新できる準備を行なっています。

2-5. display_lights_off, display_lights_on

続いて"Light"のオンオフの処理を見てみましょう。

display.c
void display_lights_off(void)
{
    ESP_LOGI(TAG,"lights off");
    xSemaphoreTake(xGuiSemaphore, portMAX_DELAY);

    lv_img_set_src(light_object, &house_off);

    xSemaphoreGive(xGuiSemaphore);
}

まずはオフのときの表示切り替えです。これは簡単ですね。house_offの画像をソースに指定してます。
続いてオンの場合です。

display.c
void display_lights_on(int h, int s, int v)
{
    xSemaphoreTake(xGuiSemaphore, portMAX_DELAY);

    lv_color_t c = lv_color_hsv_to_rgb(h,s,v);

    lv_canvas_set_palette(window_object,1,c);

    lv_img_set_src(light_object, &house_on);

    xSemaphoreGive(xGuiSemaphore);
}

スイッチがオンの場合の処理では引数にHSVカラーを受け取り、lv_color_hsv_to_rgbにてRGBに変換した後、lv_canvas_set_paletteにて窓部分の塗りつぶしパレットの色を更新しています。このように窓の部分の色をリモコンに追随させています。
で、house_onの画像に差し替えています。

このようにオンの処理で窓の部分の色を変更しています。

2-6. display_fan_speed, display_fan_off, display_fan_on

Fanの表示切り替え部分の処理は以下のようになっております。

display.c
void display_fan_speed(int s)
{
    lv_slider_set_value(fan_strength_slider,s,LV_ANIM_OFF);
    ESP_LOGI(TAG,"fan spinning %d",s);
    g_fan_speed = s;
}

void display_fan_off()
{
    ESP_LOGI(TAG,"fan off");
    lv_switch_off(fan_sw1, LV_ANIM_OFF);
    g_fan_power = false;
}

void display_fan_on()
{
    ESP_LOGI(TAG,"switch on");
    lv_switch_on(fan_sw1, LV_ANIM_OFF);
    g_fan_power = true;
}

Fanの画面要素についてはコントロールとイベントハンドラがバインドされているのでコントロールのプロパティを変更するだけでOKですね。

2-7. display_temperature

さて、温度計部分の表示処理です。以下のようになっております。

display.c
void display_temperature(float c)
{
    const float maxTemp_c = 100.0;
    const float minTemp_c = 0.0;

    int rect_height = (int)(((float)THREAD_HEIGHT * (c - minTemp_c)) / (maxTemp_c - minTemp_c));

    xSemaphoreTake(xGuiSemaphore, portMAX_DELAY);

    lv_color_t tc;
    tc.full = 0;
    lv_canvas_fill_bg(thread_object,tc,LV_OPA_100);

    tc.full = 1;

    for(int x=0;x<THREAD_WIDTH;x++)
    {
        for(int y=0;y<rect_height;y++)
        {
            lv_canvas_set_px(thread_object,x,THREAD_HEIGHT-y,tc);
        }
    }

    lv_label_set_text_fmt(temperature_label,"%dC",(int)c);

    xSemaphoreGive(xGuiSemaphore);
}

ついにCanvasに対して直接、描画する処理が出てまいりました。

rect_heightで温度に応じた矩形の高さを求め、lv_canvas_fill_bgでいったん背景をクリアにするか塗りつぶし、lv_canvas_set_pxで1点ずつ描画しているように見えますね。lv_color_tの使い方やパレットカラーの使い方がわからないのでなんとなくですが。。。

そして、lv_label_set_text_fmtにて、floatで渡された温度c%dで整数として表示するようにしています。

表示の処理は大体、以上となります。
説明は省略しておりましたが各所でxGuiSemaphoreをチェックして描画処理がバッティングしないようにしております。xSemaphoreTakexSemaphoreGiveでセマフォを取ったりあげたりしているわけです。
なるほど、大変だこりゃ。

3. fan.*→"Fan機器"の制御

いよいよやって参りましたFan機器の制御部分です。
まず、タッチパネル画面に表示する画像データを定義するファイルがいくつかありますが、以下では中身の解説は省略いたします。
"fan_1.c"〜"fan6.c"、"fan_off.c"などのファイルでは画像データがベタ書きされております。これについては特にいうことはないかと思います。以降の機器についても画像データを定義する"*.c"ファイルがありますがこれについての紹介は省略させていただきます。

ではまず、fan.hの中身で全体像を把握いたしましょう。

fan.h
...
#include <esp_rmaker_core.h>

void fan_init(esp_rmaker_node_t *node);
void fan_set_speed(int speed);
void fan_set_power(bool power);
void fan_power_report(void);
void fan_speed_report(void);
...

RainMakerエージェントに対する初期化処理と、パワーのオンオフ、スピードの変更処理に加えて、パワーの状態とスピードの状態を通知する処理があるようですね。

ではそれぞれの処理について見ていきましょう。

3-1. fan_init

"Fan機器"の初期化処理です。以下のようになっております。

fan.c
void fan_init(esp_rmaker_node_t *node)
{
    /* Create a Fan device and add the relevant parameters to it */
    fan_device = esp_rmaker_fan_device_create("Fan", NULL, DEFAULT_FAN_POWER);
    esp_rmaker_device_add_cb(fan_device, fan_cb, NULL);
    esp_rmaker_device_add_param(fan_device, esp_rmaker_speed_param_create(ESP_RMAKER_DEF_SPEED_NAME, DEFAULT_FAN_SPEED));
    esp_rmaker_node_add_device(node, fan_device);

    display_fan_init();
}

esp_rmaker_fan_device_createで"Fan機器"の定義を行なっております。関数がFan機器専用になっておりますのでこの中身について軽く触れておきましょう。

components/esp_rainmaker/src/standard_types/esp_rmaker_standard_devices.c
esp_rmaker_device_t *esp_rmaker_fan_device_create(const char *dev_name,
        void *priv_data, bool power)
{
    esp_rmaker_device_t *device = esp_rmaker_device_create(dev_name, ESP_RMAKER_DEVICE_FAN, priv_data);
    if (device) {
        esp_rmaker_device_add_param(device, esp_rmaker_name_param_create(ESP_RMAKER_DEF_NAME_PARAM, dev_name));
        esp_rmaker_param_t *primary = esp_rmaker_power_param_create(ESP_RMAKER_DEF_POWER_NAME, power);
        esp_rmaker_device_add_param(device, primary);
        esp_rmaker_device_assign_primary_param(device, primary);
    }
    return device;
}

esp_rmaker_device_createで機器の登録を行い、ESP_RMAKER_DEF_NAME_PARAM=名称パラメータとESP_RMAKER_DEF_POWER_NAME=電源パラメータを追加、そして電源パラメータのほうを"primary_param"に"assign"しています。primary_paramの具体的な効能はわかりませんが、これが"主たるパラメータ"なのでしょう。

さて元のfan_initに戻りましょう。

続いて重要な処理がesp_rmaker_device_add_cbです。スマホのRainMakerアプリ上での操作に対するコールバック関数を指定しています。
このコールバック関数については次に紹介します。

続いてesp_rmaker_device_add_paramで"Fan機器"に対して、ESP_RMAKER_DEF_SPEED_NAME=スピードパラメタを追加しています。

そしてこれらの機器設定をまとめて、esp_rmaker_node_add_deviceでRainMaker Agentに通知しています。

これらの処理が完了したのち、display_fan_init()を呼び出して表示の初期化処理を行なっております。

Fan機器の初期化の処理については以上ですね。あの振動モーターは特別な初期化処理なしで使えるのでしょう。

ではコールバック関数について紹介いたします。

3-1-1. fan_cb

コールバック関数は以下のようになっております。

fan.c
static esp_err_t fan_cb(const esp_rmaker_device_t *device, const esp_rmaker_param_t *param,
            const esp_rmaker_param_val_t val, void *priv_data, esp_rmaker_write_ctx_t *ctx)
{
    if (ctx) {
        ESP_LOGI(TAG, "Received write request via : %s", esp_rmaker_device_cb_src_to_str(ctx->src));
    }
    const char *device_name = esp_rmaker_device_get_name(device);
    const char *param_name = esp_rmaker_param_get_name(param);
    if (strcmp(param_name, ESP_RMAKER_DEF_POWER_NAME) == 0) {
        ESP_LOGI(TAG, "Received value = %s for %s - %s",
                val.val.b? "true" : "false", device_name, param_name);
        fan_set_power(val.val.b);
    } else if (strcmp(param_name, ESP_RMAKER_DEF_SPEED_NAME) == 0) {
        ESP_LOGI(TAG, "Received value = %d for %s - %s",
                val.val.i, device_name, param_name);
        fan_set_speed( val.val.i);
    } else {
        /* Silently ignoring invalid params */
        return ESP_OK;
    }
    esp_rmaker_param_update_and_report(param, val);
    return ESP_OK;
}

いろいろと処理を行なっているように見えますがキモはparam_nameの値によってfan_set_powerfan_set_speedを呼び出しています。パラメタ名に応じた処理関数の呼び出しを切り替えているわけです。
またここで処理に応じて値の"データ型"が変わるので、val.val.bval.val.iかを渡すのも切り替えています。

3-2. fan_set_speed

fan_set_speedはスピードが変化したときの機器の制御処理です。

fan.c
void fan_set_speed(int speed)
{
    g_fan_speed = speed;
    if(g_fan_power)
    {
        Core2ForAWS_Motor_SetStrength(g_fan_speed * 20);
    }
    display_fan_speed(g_fan_speed);
}

振動モーターの強さを変えて表示を変えているだけですね。
これだけではつまらないのでCore2ForAWS_Motor_SetStrengthの中身も紹介します。

3-2-1. Core2ForAWS_Motor_SetStrength

モーターの制御はここで行なっています。

components/core2forAWS/core2forAWS.c
void Core2ForAWS_Motor_SetStrength(uint8_t strength) {
    static bool motor_state = false;
    if (strength > 100) {
        strength = 100;
    }
    uint16_t volt = (uint32_t)strength * (AXP192_LDO_VOLT_MAX - AXP192_LDO_VOLT_MIN) / 100 + AXP192_LDO_VOLT_MIN;
    Axp192_SetLDO3Volt(volt);
    if (strength == 0 && motor_state == true) {
        motor_state = false;
        Axp192_EnableLDO3(0);
    } else if(motor_state == false) {
        motor_state = true;
        Axp192_EnableLDO3(1);
    }
}

AXP192が振動モーターのようです。で、渡されたstrengthに応じた電圧を計算して、Axp192_SetLDO3Voltしてます。
motor_statestaticで状態が保存されるようになっており、voltが0になったらAxp192_EnableLDO3(0)でモーターオフ、そうでなければAxp192_EnableLDO3(1)でモーターがオンになるようにしております。
なるほど、電圧の高さを調整することで振動モーターの振動の強さとオンオフ同時に制御してるんですね。

3-3. fan_power_report, fan_speed_report

RainMaker Agentを通じてMQTTによるパラメタの通知します。

fan.c
void fan_power_report(void)
{
    esp_rmaker_param_update_and_report(
            esp_rmaker_device_get_param_by_type(fan_device, ESP_RMAKER_PARAM_POWER),
            esp_rmaker_bool(g_fan_power));
}

void fan_speed_report(void)
{
    esp_rmaker_param_update_and_report(
        esp_rmaker_device_get_param_by_type(fan_device, ESP_RMAKER_PARAM_SPEED),
        esp_rmaker_int(g_fan_speed));
}

esp_rmaker_param_update_and_reportが通知する関数ですが、ここでも通知する"値の型"に応じてesp_rmaker_boolesp_rmaker_intなどのように値のシリアライズ?方法が変わるようです。

3-4. fan_set_power

パワーのオンオフした場合の処理です。

fan.c
void fan_set_power(bool power)
{
    g_fan_power = power;
    if (power) {
        int speed = g_fan_speed * 20;
        ESP_LOGI(TAG,"power up, speed=%d",speed);
        Core2ForAWS_Motor_SetStrength(speed);
        display_fan_on();
    } else {
        Core2ForAWS_Motor_SetStrength(0);
        display_fan_off();
    }
}

パワーがオンのときに初期スピードを設定して振動モーターをオンにし、パワーがオフの場合は振動モーターの強さ0にしています。
同時に表示の切り替え処理も呼び出しております。

"Fan機器"の制御は以上です。

4. hsv2rgb.*→HSVとRBGの色空間変換

ここではこの変換アルゴリズムについて本質ではないので実装をさらっと紹介して終わります。

hsv2grb.c
uint32_t hsv2rgb(uint16_t h, uint8_t s, uint8_t v)
{
    h = (uint32_t)((uint32_t)h * 255) / 360;
    s = (uint16_t)((uint16_t)s * 255) / 100;
    v = (uint16_t)((uint16_t)v * 255) / 100;

    uint8_t r, g, b;

    uint8_t region, remainder, p, q, t;

    if(s == 0) {
        return v<<24|v<<16|v<<8;
    }

    region    = h / 43;
    remainder = (h - (region * 43)) * 6;

    p = (v * (255 - s)) >> 8;
    q = (v * (255 - ((s * remainder) >> 8))) >> 8;
    t = (v * (255 - ((s * (255 - remainder)) >> 8))) >> 8;

    switch(region) {
        case 0:
            r = v;
            g = t;
            b = p;
            break;
        case 1:
            r = q;
            g = v;
            b = p;
            break;
        case 2:
            r = p;
            g = v;
            b = t;
            break;
        case 3:
            r = p;
            g = q;
            b = v;
            break;
        case 4:
            r = t;
            g = p;
            b = v;
            break;
        default:
            r = v;
            g = p;
            b = q;
            break;
    }

    return r<<16|g<<8|b;
}

これはもう、こういうことですね・・・。はい。

5. light.*→"Light機器"の制御

Light機器の制御処理についてついて見ていきましょう。
例によってlight.hにて全体像の把握からです。

light.h
#include <esp_rmaker_core.h>

void light_init(esp_rmaker_node_t *node);

void light_set_on(bool on);
void light_set_hue(int hue);
void light_set_saturation(int hue);
void light_set_brightness(int brightness);

bool light_is_on(void);
int light_get_hue(void);
int light_get_saturation(void);
int light_get_brightness(void);

なるほど、照明の調光はCore2本体では行えないので、スマホからの指示を受けて制御するのみとなっておりますね。

5-1. light_init

"Light機器"の初期化処理は以下のようになっております。

light.c
void light_init(esp_rmaker_node_t *node)
{
    display_house_init();

    Core2ForAWS_Sk6812_Init();

    Core2ForAWS_Sk6812_SetBrightness(255);

    light_set_on(false);

    /* Create a Light device and add the relevant parameters to it */
    light_device = esp_rmaker_lightbulb_device_create("Light", NULL, DEFAULT_LIGHT_POWER);
    esp_rmaker_device_add_cb(light_device, light_cb, NULL);
    
    esp_rmaker_device_add_param(light_device, esp_rmaker_brightness_param_create(ESP_RMAKER_DEF_BRIGHTNESS_NAME, DEFAULT_LIGHT_BRIGHTNESS));
    esp_rmaker_device_add_param(light_device, esp_rmaker_hue_param_create(ESP_RMAKER_DEF_HUE_NAME, DEFAULT_LIGHT_HUE));
    esp_rmaker_device_add_param(light_device, esp_rmaker_saturation_param_create(ESP_RMAKER_DEF_SATURATION_NAME, DEFAULT_LIGHT_SATURATION));

    esp_rmaker_device_add_attribute(light_device, "Serial Number", "012345");
    esp_rmaker_device_add_attribute(light_device, "MAC", "xx:yy:zz:aa:bb:cc");

    esp_rmaker_node_add_device(node, light_device);
}

Fan機器とほぼ同様ですが、ざっくりと流れをご紹介いたします。

  1. esp_rmaker_lightbulb_device_createでLight機器の定義
  2. esp_rmaker_device_add_cbでコールバックを登録
  3. BRIGHTNESSHUESATURATIONのパラメタを追加
  4. "Serial Number"、"MAC"の"属性値"(読み取り専用)を追加
  5. AgentにLight機器を通知

esp_rmaker_device_add_attributeにて属性が追加できるのですね。

続いてコールバックについてもさっとご紹介いたします。

5-1-1. light_cb

コールバックは以下のようになっております。

light.c
static esp_err_t light_cb(const esp_rmaker_device_t *device, const esp_rmaker_param_t *param,
            const esp_rmaker_param_val_t val, void *priv_data, esp_rmaker_write_ctx_t *ctx)
{
    if (ctx) {
        ESP_LOGI(TAG, "Received write request via : %s", esp_rmaker_device_cb_src_to_str(ctx->src));
    }
    const char *device_name = esp_rmaker_device_get_name(device);
    const char *param_name = esp_rmaker_param_get_name(param);
    if (strcmp(param_name, ESP_RMAKER_DEF_POWER_NAME) == 0) {
        ESP_LOGI(TAG, "Received value = %s for %s - %s",
                val.val.b? "true" : "false", device_name, param_name);
        light_set_on(val.val.b);
    } else if (strcmp(param_name, ESP_RMAKER_DEF_BRIGHTNESS_NAME) == 0) {
        ESP_LOGI(TAG, "Received value = %d for %s - %s",
                val.val.i, device_name, param_name);
        light_set_brightness(val.val.i);
    } else if (strcmp(param_name, ESP_RMAKER_DEF_HUE_NAME) == 0) {
        ESP_LOGI(TAG, "Received value = %d for %s - %s",
                val.val.i, device_name, param_name);
        light_set_hue(val.val.i);
    } else if (strcmp(param_name, ESP_RMAKER_DEF_SATURATION_NAME) == 0) {
        ESP_LOGI(TAG, "Received value = %d for %s - %s",
                val.val.i, device_name, param_name);
        light_set_saturation(val.val.i);
    } else {
        /* Silently ignoring invalid params */
        return ESP_OK;
    }
    esp_rmaker_param_update_and_report(param, val);
    return ESP_OK;
}

ここでも基本はパラメタに応じて呼び出す処理とデータ型の指定です。bool型はval.val.b、int型はval.val.iとなっております。

5-2. light_set_on

Light機器のメインとなる電源オン・オフの処理です。

light.c
void light_set_on(bool on)
{
    isOn = on;
    if(on)
    {
        //uint32_t color = hsb_to_rgb_int(hue,saturation,brightness);
        uint32_t color = hsv2rgb(hue,saturation, brightness);

        Core2ForAWS_Sk6812_SetSideColor(SK6812_SIDE_LEFT,  color);
        Core2ForAWS_Sk6812_SetSideColor(SK6812_SIDE_RIGHT, color);
        Core2ForAWS_Sk6812_Show();
        display_lights_on(hue,saturation,brightness);
    }
    else
    {
        Core2ForAWS_Sk6812_SetSideColor(SK6812_SIDE_LEFT,  0);
        Core2ForAWS_Sk6812_SetSideColor(SK6812_SIDE_RIGHT, 0);
        Core2ForAWS_Sk6812_Show();
        display_lights_off();
    }    
}

やっていることはシンプルですね。
オンの場合は、HSVからRGBを求めて、LEDであるSK6812に色指定をして点灯、画面の更新、です。
オフの場合は、LEDを"真っ黒にして点灯"=消灯、画面の更新、です。

Core2ForAWS_Sk6812_SetSideColorCore2ForAWS_Sk6812_Showについても掘っていきましょう。

5-2-1. Core2ForAWS_Sk6812_*→LEDの制御

LEDの制御部分についても簡単にご紹介します。

components/core2forAWS/core2forAWS.c
...
void Core2ForAWS_Sk6812_SetSideColor(uint8_t side, uint32_t color) {
    if (side == SK6812_SIDE_LEFT) {
        for (uint8_t i = 5; i < 10; i++) {
            np_set_pixel_color(&px, i, color << 8);
        }
    } else {
        for (uint8_t i = 0; i < 5; i++) {
            np_set_pixel_color(&px, i, color << 8);
        }
    }
}
...
void Core2ForAWS_Sk6812_Show() {
    np_show(&px, RMT_CHANNEL_0);
}
...

面白いのはnp_set_pixel_colorに渡すインデクス0〜4が右側のLED、5〜9が左側のLEDとなっている箇所、そしてuint32_tcolorを8bitシフトして先頭から24bitの整数として渡していますね。
そしてCore2ForAWS_Sk6812_Showではnp_showpxの色データを送信している模様です。
プログラムやデータが回路の構造に合わせて調整されているのがよくわかります。

5-3. Setter/Getter

残りの関数はSetter/Getterとなっておりますのでさっとご紹介いたします。

light.c
void light_set_hue(int h)
{
    hue = h;
    light_set_on(true);
}

void light_set_saturation(int s)
{
    saturation = s;
    light_set_on(true);
}

void light_set_brightness(int b)
{
    brightness = b;
    light_set_on(true);
}

bool light_is_on(void)
{
    return isOn;
}

int light_get_hue(void)
{
    return hue;
}

int light_get_saturation(void)
{
    return saturation;
}

int light_get_brightness(void)
{
    return brightness;
}

後半のGetterは特筆するべきことはないですが、前半のlight_set_huelight_set_saturationlight_set_brightnessのSetterではそれぞれ値をセットした後にlight_set_on(true);が呼ばれており、上記でご紹介いたしましたLEDの点灯処理が走っております。
Light機器では調光処理の呼び出しはSetter側からキックで統一している、ということですね。

6. temperature.* → "Temperature"機器の制御

いよいよ最後の機器である "Temperature"についてです。
早速、temperature.hを見て見ましょう。

temperature.h
#include <esp_rmaker_core.h>

void temperature_init(esp_rmaker_node_t *node);

はい、温度計に関しては"見るだけ"なのでも、外部から見える処理は初期化しかありません。

それでは実装はどうなっているのでしょうか。見ていきましょう。

6-1. temperature_init

温度センサーは読み取り専用ですが非常に重要な機能である"タイマーによる定時観測"を行なう実装を含んでおります。
さて肝心の初期化処理は以下のようになっております。

temperature.c
void temperature_init(esp_rmaker_node_t *node)
{
    MPU6886_Init();
    /* Create a Temperature Sensor device and add the relevant parameters to it */
    temp_sensor_device = esp_rmaker_temp_sensor_device_create("Temperature Sensor", NULL, DEFAULT_TEMPERATURE);
    esp_rmaker_node_add_device(node, temp_sensor_device);

    /* Start the update task to send the temperature every so often */
    esp_timer_create_args_t sensor_timer_conf = {
        .callback = temperature_sensor_update,
        .dispatch_method = ESP_TIMER_TASK,
        .name = "temperature_sensor_update_tm"
    };
    if (esp_timer_create(&sensor_timer_conf, &sensor_timer) == ESP_OK) {
        esp_timer_start_periodic(sensor_timer, 250000U);
    }

    display_temperature_init();
}

まずMPU6886_Init();にて6軸モーションセンサー兼温度センサーである "MPU6886"の初期化処理が行われます。
続いてesp_rmaker_temp_sensor_device_createで"Temprature機器"の宣言とAgentへの通知が行われます。これについてはもういいでしょう。

さて、次の箇所ですね。esp_timer_create_args_tでタスクを定義し、esp_timer_createでタイマーの作成、esp_timer_start_periodicでタイマーの開始、の手続きとなっております。
esp_timer_start_periodicの第二引数は"マイクロ秒"です。つまりここでは250msを指定している、ということでしょう。
このような手順で定時監視のタスクを作成するようです。

そしてdisplay_temperature_initで画面表示の初期化処理が呼び出されます。

それではタイマーで呼び出されるタスクの実装を見て見ましょう。

temperature.c
static void temperature_sensor_update(void *priv)
{
    const int reportReload = REPORTING_PERIOD * 4;
    static int reportingCount = reportReload;
    float temperature;
    float avg_temperature = 0;

    MPU6886_GetTempData(&temperature);

    avg_temperature = floatFIR(temperature);

    display_temperature(avg_temperature);

    reportingCount --;
    if(!reportingCount)
    {
        esp_rmaker_param_update_and_report(
                    esp_rmaker_device_get_param_by_type(temp_sensor_device, ESP_RMAKER_PARAM_TEMPERATURE),
                    esp_rmaker_float(avg_temperature));
        reportingCount = reportReload;
    }
}

MPU6886_GetTempDataで温度データを読み込んでいます。
その後、floatFIRという処理を行なっており、temperatureについてなんらかの処理を行なってavg_temperatureとしています。この処理の中身については本質ではないので触れないでおきましょう。
まぁ、"観測データの補正"を行なっているのでしょう。
続いて、"display_temperature"で温度計部分の表示の書き換え処理を呼び出しています。

その後、reportingCountを減算し、ゼロになったらesp_rmaker_param_update_and_reportで温度の値をブロードキャストしています。
画面の表示とアプリへの通知はタイミングが違う、というか毎度、AWSにあげるのはコストが高いので何回かに1回に通知処理を呼び出すようにしているようです。

以上でざっくりではございますが、Getting Startedプロジェクトに含まれる機器の制御処理についてご紹介いたしました。

まとめ

Getting Started、いやはや侮るなかれでございます。Getting Startedにしては大変、中身の濃いというか極めて重要な基本機能がいろいろてんこ盛りでございました。
"機器"が3つも出てきますがそれぞれ以下のようなポイントがあるかと思います。

  • Light:スマホからの制御、画像とキャンバスでの塗りつぶしの重ね合わせ、LEDの制御、機器の"属性値"の設定
  • Fan:スマホとタッチスクリーンの双方向での制御、画像切り替えによるアニメーション、GUIコンポーネント(スイッチ、スライダー)、振動モーターの制御
  • Temperature:ESPでの高精度タイマー、キャンバスへの直接描画、文字列の表示制御、MPU6886センサー値の読み取り

いや〜Getting Startedなのにめちゃくちゃ濃いプロジェクトでした。
センサーというかチップによって扱いや振る舞いが異なるので、それぞれしっかりと取り扱い方を押さえておかなければなりませんね。。。
またRainMakerのおかげでスマホとのやり取りに関してはほぼ気にすることなく処理が書けるかと思います。

まだまだ触ってみた、だけのレベルではございますが、M5Stack Core2入門・後編はここまでにしたいと思います。

お疲れ様でした!

Discussion