Zenn
🐄

【LvGL】Arduino(Wio Terminal)での使い方を抽象化しようとした

2025/03/08に公開

LvGLLvGLを使いやすくまとめたかった

ArduinoArduino開発で使われるプログラミング言語は、殆どの場合CC++C/C++になる。CCC++C++の何が相違点は幾つもあるが、その代表的なものとしてオブジェクト指向がある。

個人的に思うオブジェクト指向の利点は、「まとまる」ことと信じている。件のLvGLLvGLは、ボタンやキーボード、ウィンドウといったWidgetWidgetが充実している。その割には、データ型が全てlv_obj_tで統一され、一見するとWidgetWidgetに区別がないようにも感じる。更に、ボタンのために存在する関数、キーボードのために存在する関数、といったものが散乱している。

オブジェクト指向の代表的な概念であるクラスを使うならば、ボタンのための関数はボタンしか呼び出せないように、ある種の実行権限のようなものが効く。このことが分かりやすさにつながるが、LvGLLvGLは特にそうした制限を設けていない。その意図としては、実装が簡素になることで軽量・高速化する意図があるものと推察する。

実際のところ、高速な言語として知られるCCRustRustなどにはクラスがない。クラスを用いることが実行速度の隘路となることもある。とは言えどうも使いづらさを覚えてしまったので、敢えてクラス化しようと思い立った。

https://github.com/amenaruya/object_oriented_lvgl_on_wio_terminal/tree/main

抽象部

抽象と雖も、WioWio TerminalTerminalTFT_eSPI.hと併用する場合しか知らないため、あらゆるArduinoArduinoに使えるというものではないだろう。

ここでは、画面や入力の「設定」を実装した。いくら表面上の「内容」、即ち画面上に配置するものの種類や数が変われど、これらの設定は大きく変わらないように見える。対して画面の内容に関わる部分は大きく変わるため、virtualを付して改変(overload)を許すか、そもそも記述しなかった。

なお純粋仮想関数__mFContentsSetup()があるため、このクラスLAALはインスタンス化できない。

LvglArduinoAbstractLayer.hpp
#ifndef __LVGL_ARDUINO_ABSTRACT_LAYER_HPP__
#define __LVGL_ARDUINO_ABSTRACT_LAYER_HPP__

#include <Arduino.h>
#include <TFT_eSPI.h>
#include <lvgl.h>

template<class T>
class LAAL {
private:
    /* delete copy constructor */
    LAAL(const LAAL&) = delete;
    /* delete copy operator */
    LAAL& operator=(const LAAL&) = delete;

protected:
    /* singleton */
    static T*           __smpLaalSingleton;

    /* TFT LCD */
    static TFT_eSPI*    __mpTft;

    /* tick (1~10) */
    uint32_t            __mu32TickPeriod;

    /* ディスプレイ */

    /* display buffer */
    lv_disp_buf_t       __smDisplayBuffer;
    /* main color buffer */
    lv_color_t          __smColorBufferMain[LV_HOR_RES_MAX * 10];
    /* sub color buffer */
    lv_color_t          __smColorBufferSub[LV_HOR_RES_MAX * 10];
    /* display driver */
    lv_disp_drv_t       __mDisplayDriver;
    /* display */
    lv_disp_t*          __mpDisplay;
    /* 画面初期化 */
    static void         __mFDisplayFlush(
        lv_disp_drv_t*      pDisplayDriver,
        const lv_area_t*    pArea,
        lv_color_t*         pColor
    );
    /* 画面設定 */
    virtual void        __mFDisplaySetup();

    /* 入力デバイス(encode) */

    /* input device driver */
    lv_indev_drv_t      __mInputDeviceDriverEncoder;
    /* input device */
    lv_indev_t*         __mpInputDeviceEncoder;
    /* Reading encoder callback */
    static bool         __mFbReadEncoder(
        lv_indev_drv_t*     pInputDeviceDriver,
        lv_indev_data_t*    pInputDevicesData
    );
    /* デバイス設定 */
    virtual void        __mFDeviceSetup();

    /* 画面要素 */

    /* widget */
    /*
    example:
    lv_obj_t*           __mpObject;
    void                __mFObjectEventhandler(
        lv_obj_t*           pObject,
        lv_event_t          event
    ) = 0;
    */

    /* group */
    /*
    example:
    lv_group_t*         __mpGroup;
    void                __mFGroupSetup();
    */

    /* 画面構成 */
    virtual void        __mFContentsSetup() = 0;

    /* constructor */
    void                __mFNew();
    LAAL() = default;
    /* destructor */
    virtual ~LAAL() = default;

public:
    /* 設定 */
    void                mFSetup();
    /* 画面実行 */
    void                mFOperateGraphic();
};

#include "LvglArduinoAbstractLayer.ipp"

#endif // __LVGL_ARDUINO_ABSTRACT_LAYER_HPP__
LvglArduinoAbstractLayer.ipp
template<class T>
T* LAAL<T>::__smpLaalSingleton = nullptr;
template<class T>
TFT_eSPI* LAAL<T>::__mpTft = nullptr;

template<class T>
void LAAL<T>::__mFNew() {
    /* lvglの初期化 */
    lv_init();

    /* TFTの初期設定 */
    (this -> __mpTft) -> begin();
    (this -> __mpTft) -> setRotation(3);
}

template<class T>
void LAAL<T>::mFSetup() {
    /* 画面設定 */
    (this -> __mFDisplaySetup)();
    /* 入力設定 */
    (this -> __mFDeviceSetup)();
    /* 画面作成 */
    (this -> __mFContentsSetup)();
}

template<class T>
void LAAL<T>::__mFDisplayFlush(
    lv_disp_drv_t*      pDisplayDriver,
    const lv_area_t*    pArea,
    lv_color_t*         pColor
) {
    const uint32_t w = pArea -> x2 - pArea -> x1 + 1;
    const uint32_t h = pArea -> y2 - pArea -> y1 + 1;

    LAAL<T>::__mpTft -> startWrite();
    LAAL<T>::__mpTft -> setAddrWindow(pArea -> x1, pArea -> y1, w, h);
    LAAL<T>::__mpTft -> pushColors(&pColor -> full, w * h, true);
    LAAL<T>::__mpTft -> endWrite();

    lv_disp_flush_ready(pDisplayDriver);
}

template<class T>
void LAAL<T>::mFOperateGraphic() {
    while (1) {
        /* operate tasks */
        this -> __mu32TickPeriod = lv_task_handler();
        /* delay */
        delay(this -> __mu32TickPeriod);
        /* tell lvgl system elapsed time */
        lv_tick_inc(this -> __mu32TickPeriod);
    }
}

template<class T>
void LAAL<T>::__mFDisplaySetup() {
    /* 画面表示用bufferそのものの初期化 */
    lv_disp_buf_init(
        &(this -> __smDisplayBuffer),
        this -> __smColorBufferMain,
        this -> __smColorBufferSub,
        LV_HOR_RES_MAX * 10
    );

    /* 画面制御設定 */
    lv_disp_drv_init(&(this -> __mDisplayDriver));
    (this -> __mDisplayDriver).ver_res = LV_VER_RES_MAX;
    (this -> __mDisplayDriver).flush_cb = this -> __mFDisplayFlush;
    (this -> __mDisplayDriver).buffer = &(this -> __smDisplayBuffer);
    this -> __mpDisplay = lv_disp_drv_register(&(this -> __mDisplayDriver));
}

template<class T>
bool LAAL<T>::__mFbReadEncoder(
    lv_indev_drv_t*     pInputDeviceDriver,
    lv_indev_data_t*    pInputDevicesData
) {
    (void) pInputDeviceDriver;
    (void) pInputDevicesData;
    return false;
}

template<class T>
void LAAL<T>::__mFDeviceSetup() {
    /* 入力の設定 */
    lv_indev_drv_init(&(this -> __mInputDeviceDriverEncoder));
    (this -> __mInputDeviceDriverEncoder).type = LV_INDEV_TYPE_ENCODER;
    (this -> __mInputDeviceDriverEncoder).read_cb = this -> __mFbReadEncoder;
    this -> __mpInputDeviceEncoder = lv_indev_drv_register(&(this -> __mInputDeviceDriverEncoder));
}

アプリケーション部

このクラスは、ボタンを表示するサンプルプログラムを実装する。書き方を変えたばかりで内容は前記事の儘である。

SimpleButtonApp.hpp
#ifndef __SIMPLE_BUTTON_APP_HPP__
#define __SIMPLE_BUTTON_APP_HPP__

#include "LvglArduinoAbstractLayer.hpp"

class App final: public LAAL<App> {
private:
    /* 画面設定 */
    void        __mFDisplaySetup();

    /* Reading encoder callback */
    static bool __mFbReadEncoder(
        lv_indev_drv_t*     pInputDeviceDriver,
        lv_indev_data_t*    pInputDevicesData
    );
    /* デバイス設定 */
    void        __mFDeviceSetup();

    /* button1 */
    lv_obj_t*   __mpObjectButton1;
    /* label1 */
    lv_obj_t*   __mpObjectLabel1;
    /* button2 */
    lv_obj_t*   __mpObjectButton2;
    /* label2 */
    lv_obj_t*   __mpObjectLabel2;
    /* button配置 */
    void        __mFButtonsSetup();

    /* group */
    lv_group_t* __mpGroup;
    /* group設定 */
    void        __mFGroupSetup();

    /* 画面構成 */
    void        __mFContentsSetup();

    /* constructor */
    App();
    /* destructor */
    ~App();

public:
    /* 初期化 */
    static App* mFpLaalInitialize(TFT_eSPI* pTft);
};

#endif // __SIMPLE_BUTTON_APP_HPP__
SimpleButtonApp.cpp
#include "SimpleButtonApp.hpp"

App* App::mFpLaalInitialize(TFT_eSPI* pTft) {
    /* 未定ならば初期化 */
    if (LAAL<App>::__smpLaalSingleton == nullptr) {
        /* static member variableの初期化 */
        LAAL<App>::__mpTft = pTft;
        /* 子クラスの初期化 */
        LAAL<App>::__smpLaalSingleton = new App();
    }
    /* 既初期化の場合でも既存インスタンスへのポインターが返却される */
    return LAAL<App>::__smpLaalSingleton;
}

App::App() {
    LAAL<App>::__mFNew();
}
App::~App() {}

void App::__mFDisplaySetup() {
    LAAL<App>::__mFDisplaySetup();
}

bool App::__mFbReadEncoder(
    lv_indev_drv_t*  pInputDeviceDriver,
    lv_indev_data_t*        pInputDevicesData
) {
    (void) pInputDeviceDriver;
    if (digitalRead(WIO_5S_PRESS) == LOW) {
        pInputDevicesData -> state = LV_INDEV_STATE_PR;
    } else {
        pInputDevicesData -> state = LV_INDEV_STATE_REL;
    }

    /*
    indev_encoder_proc()に基づく左右の定義
    */
    if (digitalRead(WIO_5S_LEFT) == LOW) {
        pInputDevicesData -> enc_diff = -1;
    } else if (digitalRead(WIO_5S_RIGHT) == LOW) {
        pInputDevicesData -> enc_diff = 1;
    } else {
        pInputDevicesData -> enc_diff = 0;
    }

    /* no data buffered */
    return false;
}

void App::__mFDeviceSetup() {
    /* 入力の設定 */
    lv_indev_drv_init(&(LAAL<App>::__mInputDeviceDriverEncoder));
    (LAAL<App>::__mInputDeviceDriverEncoder).type = LV_INDEV_TYPE_ENCODER;
    (LAAL<App>::__mInputDeviceDriverEncoder).read_cb = App::__mFbReadEncoder;
    LAAL<App>::__mpInputDeviceEncoder = lv_indev_drv_register(&(LAAL<App>::__mInputDeviceDriverEncoder));

    /* enable 5way switch */
    pinMode(WIO_5S_UP, INPUT);
    pinMode(WIO_5S_DOWN, INPUT);
    pinMode(WIO_5S_LEFT, INPUT);
    pinMode(WIO_5S_RIGHT, INPUT);
    pinMode(WIO_5S_PRESS, INPUT);
}

void App::__mFButtonsSetup() {
    this -> __mpObjectButton1 = lv_btn_create(lv_scr_act(), NULL);
    // lv_obj_set_event_cb(this -> __mpObjectButton1, event_handler);
    lv_obj_align(this -> __mpObjectButton1, NULL, LV_ALIGN_CENTER, 0, -40);

    this -> __mpObjectLabel1 = lv_label_create(this -> __mpObjectButton1, NULL);
    lv_label_set_text(this -> __mpObjectLabel1, "Button");

    this -> __mpObjectButton2 = lv_btn_create(lv_scr_act(), NULL);
    // lv_obj_set_event_cb(this -> __mpObjectButton2, event_handler);
    lv_obj_align(this -> __mpObjectButton2, NULL, LV_ALIGN_CENTER, 0, 40);
    lv_btn_set_checkable(this -> __mpObjectButton2, true);
    lv_btn_toggle(this -> __mpObjectButton2);
    lv_btn_set_fit2(this -> __mpObjectButton2, LV_FIT_NONE, LV_FIT_TIGHT);

    this -> __mpObjectLabel2 = lv_label_create(this -> __mpObjectButton2, NULL);
    lv_label_set_text(this -> __mpObjectLabel2, "Toggled");
}

void App::__mFGroupSetup() {
    this -> __mpGroup = lv_group_create();
    lv_indev_set_group(LAAL<App>::__mpInputDeviceEncoder, this -> __mpGroup);
    lv_group_add_obj(this -> __mpGroup, this -> __mpObjectButton1);
    lv_group_add_obj(this -> __mpGroup, this -> __mpObjectButton2);
}

void App::__mFContentsSetup() {
    (this -> __mFButtonsSetup)();
    (this -> __mFGroupSetup)();
}

.inoファイル

通常のやり方でサンプルプログラムを真似た場合、.inoファイルだけであるとはいえ200200300300行程度に上る。しかしこうしてクラス化したことで、表面上は2020行に満たないファイルとなった。

#include <TFT_eSPI.h>
#include <lvgl.h>

#include "SimpleButtonApp.hpp"

/* TFT LCD */
static TFT_eSPI TFT;

/* App */
App* pApp;

void setup() {
    pApp = App::mFpLaalInitialize(&TFT);
    pApp -> mFSetup();
}

void loop() {
    pApp -> mFOperateGraphic();
}

Discussion

ログインするとコメントできます