Zenn
🌠

【Wio Terminal/LvGL】センサーの値をメーター風に可視化

2025/03/13に公開

センサーの値を画面に反映する

LvGLLvGLで作るGUIGUIには、ボタンやキーボードのように人間の操作を受け付けるものもあれば、ラベルやメーターのように表示するだけのものもある。これまでスライダーキーボードボタンなどが操作できることを確認したが、画面が自発的に変化するものは未だ確認していない。

その簡単な事例として、本記事ではセンサーを用いる。通常ArduinoArduino等では、何らかのセンサーを別途用意する必要がある。しかしWioWio TerminalTerminalには照度センサー(LightLight SensorSensor)が搭載されているため、これをそのまま使えばよい。

Hardware-Overview
引用:https://wiki.seeedstudio.com/Wio-Terminal-Getting-Started/

センサーから取得した値を画面に表示するに当たり、単に文字で数値を表示するだけであれば、態々しくLvGLLvGLなどと言う瀟洒なものを使う必要がない。そこで、メーターを使って数値を表現することを試みた。

サンプル集の中には、アニメーションを使ってメーターを動かしているものがある。

https://docs.lvgl.io/8.4/examples.html#meter

本来であればこれを参考にすればよいはずだが、WioWio TerminalTerminalで使われるSeeed_Arduino_LvGLには、7.0.27.0.2バージョンのLvGLLvGLが使われている。ドキュメントによれば、「MeterMeter」が存在するのはバージョン88以降のものである。バージョン77では似たものとして「GaugeGauge」があるため、これを使うしかない。

プログラム

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

先にオブジェクト指向での抽象化を試みた通り、実装を分けている。

LvglArduinoAbstractLayer

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

LightGaugeApp

LightGaugeApp.hpp
#ifndef __LIGHT_GAUGE_APP_HPP__
#define __LIGHT_GAUGE_APP_HPP__

#include "LvglArduinoAbstractLayer.hpp"

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

    /* デバイス設定 */
    void                __mFDeviceSetup();
    /* reading light sensor */
    static uint32_t     __mFu32GetLightSensorValue();

    /* gauge */
    static lv_obj_t*    __mpObjectGaugeLight;
    /* gauge配置 */
    void                __mFGaugeSetup();

    /* task object */
    lv_task_t*          __mpTaskUpdateGauge;
    /* task function */
    static void         __mFUpdateGauge(lv_task_t* pTask);
    /* set task */
    void                __mFSetTask();

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

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

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

#endif // __LIGHT_GAUGE_APP_HPP__

LightGaugeApp.cpp
#include "LightGaugeApp.hpp"

/* 針の数 */
constexpr uint16_t      __NEEDLE_COUNT__ = 1;
/* 針の色 */
constexpr lv_color_t    __NEEDLE_COLOR__[__NEEDLE_COUNT__] = {LV_COLOR_SILVER};

/* gauge */
lv_obj_t*               App::__mpObjectGaugeLight = nullptr;

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

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 light sensor */
    pinMode(WIO_LIGHT, INPUT);
}

uint32_t App::__mFu32GetLightSensorValue() {
    /* センサー値を取得する */
    return analogRead(WIO_LIGHT);
}

void App::__mFGaugeSetup() {
    /* create gauge */
    this -> __mpObjectGaugeLight = lv_gauge_create(lv_scr_act(), NULL);
    /* set needle count 1 */
    lv_gauge_set_needle_count(this -> __mpObjectGaugeLight, __NEEDLE_COUNT__, __NEEDLE_COLOR__);
    /* set gauge size */
    lv_obj_set_size(this -> __mpObjectGaugeLight, 200, 200);
    /* set align center */
    lv_obj_align(this -> __mpObjectGaugeLight, NULL, LV_ALIGN_CENTER, 0, 0);
    /* set gauge range */
    lv_gauge_set_range(this -> __mpObjectGaugeLight, 0, 1100);
}

void App::__mFUpdateGauge(lv_task_t* pTask) {
    (void) pTask;
    lv_gauge_set_value(App::__mpObjectGaugeLight, 0, (App::__mFu32GetLightSensorValue)());
}

void App::__mFSetTask() {
    this -> __mpTaskUpdateGauge = lv_task_create(App::__mFUpdateGauge, 50, LV_TASK_PRIO_HIGHEST, NULL);
}

void App::__mFContentsSetup() {
    (this -> __mFGaugeSetup)();
    (this -> __mFSetTask)();
}

taskの注意:staticとリンクエラー

センサーの値を取得するに当たり、taskを作っている。

void App::__mFSetTask() {
    this -> __mpTaskUpdateGauge = lv_task_create(App::__mFUpdateGauge, 50, LV_TASK_PRIO_HIGHEST, NULL);
}

この時、lv_task_create()の第一引数には、taskとして動作する関数を指定する。但し、指定する関数はstaticでなければならないため、App::__mFUpdateGauge()staticにする。staticとしたクラスメンバー関数の中で、同クラスのメンバー変数やメンバー関数を使う場合、それらもまたstaticにしなければならない。

staticか否かによって、次のように書き分ける。

static non-static
App::__mpObjectGaugeLight this -> __mpObjectGaugeLight

このような修正を施した後、次のようなリンクエラーが出た。

C:\Users\\AppData\Local\arduino\sketches\DC395E2A554803459821E977C51E7A61\sketch\LightGaugeApp.cpp.o: In function `App::__mFUpdateGauge(_lv_task_t*)':
C:\\light_gauge/LightGaugeApp.cpp:64: undefined reference to `App::__mpObjectGaugeLight'
C:\Users\⋯\AppData\Local\arduino\sketches\DC395E2A554803459821E977C51E7A61\sketch\LightGaugeApp.cpp.o: In function `lv_gauge_set_range':
c:\\libraries\Seeed_Arduino_LvGL\src/src/lv_widgets/lv_gauge.h:105: undefined reference to `App::__mpObjectGaugeLight'
collect2.exe: error: ld returned 1 exit status

App::__mpObjectGaugeLightに関するリンクエラーであるため、次を明記しておくと解決する。

lv_obj_t*               App::__mpObjectGaugeLight = nullptr;

staticとなったものはプログラムの始まりと共に存在するが、存在することと使える状態であることとは別の問題である。この時差が問題になったものと推察する。

.inoファイル

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

#include "LightGaugeApp.hpp"

/* TFT LCD */
static TFT_eSPI TFT;

/* App */
App* pApp;

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

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

実行結果

実行結果

センサーは筐体背面側にあるため、裏から光を当てている。

Discussion

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