Zenn
🕹️

【Wio Terminal/LvGL】Button and focus handling using a encoder

2025/03/03に公開

スイッチ(物理)でボタン(画面内)操作とフォーカス

ボタンを押すだけのことだが、案外苦労した。

interface
引用:https://www.switch-science.com/products/6360

WioWio TerminalTerminalには「55-WayWay SwitchSwitch」と「UserUser ProgrammableProgrammable ButtonsButtons」があるが、題にある「スイッチ」はこの「55-WayWay SwitchSwitch」である。但しLvGLLvGLにも「SwitchSwitch」という概念があるため、極力混同を避ける意図で、プログラム中には「JoyJoy SwitchSwitch」という表現を用いた。

なお「ボタン」はLvGLLvGLの「ButtonButton」であり、「UserUser ProgrammableProgrammable ButtonsButtons」ではない。

プログラム

https://docs.lvgl.io/7.11/widgets/btn.html#simple-buttons

概形はこの例を参考にした。動作はほとんどこの通りのため、実行の様子は省略している。

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

static TFT_eSPI         TFT;
static lv_disp_buf_t    DISP_BUF;
static lv_color_t       COLOR_BUF[LV_HOR_RES_MAX * 10];

/* input device driver */
static lv_indev_drv_t   INPUT_DEVICE_DRIVER_JOY_SWITCH;
/* input device */
static lv_indev_t*      INPUT_DEVICE_JOY_SWITCH = nullptr;
/* joy switch group */
static lv_group_t*      GROUP_JOY_SWITCH = nullptr;

/* button & label */

static lv_obj_t*        BUTTON1 = nullptr;
static lv_obj_t*        LABEL_1 = nullptr;

static lv_obj_t*        BUTTON2 = nullptr;
static lv_obj_t*        LABEL_2 = nullptr;

/* mode表示用 */
static lv_obj_t*        LABEL_GROUPS_MODE = nullptr;

void display_flush(
    lv_disp_drv_t*      disp,
    const lv_area_t*    area,
    lv_color_t*         color_p
);

bool read_encoder(
    lv_indev_drv_t*     indev,
    lv_indev_data_t*    data
);

static void event_handler(lv_obj_t * obj, lv_event_t event);

void set_contents();

void set_buttons();

void set_groups();

void set_label();
void update_label();

void backend_setup();

void set_buttons() {
    /* ボタン1 */
    BUTTON1 = lv_btn_create(lv_scr_act(), NULL);
    /* callback設定(無意味) */
    lv_obj_set_event_cb(BUTTON1, event_handler);
    /* 画面中央から上方にずれた位置に配置する */
    lv_obj_align(BUTTON1, NULL, LV_ALIGN_CENTER, 0, -40);
    /* ボタン1の文字 */
    LABEL_1 = lv_label_create(BUTTON1, NULL);
    /* "Button"と設定する */
    lv_label_set_text(LABEL_1, "Button");

    /* ボタン2 */
    BUTTON2 = lv_btn_create(lv_scr_act(), NULL);
    /* callback設定(無意味) */
    lv_obj_set_event_cb(BUTTON2, event_handler);
    /* 画面中央から下方にずれた位置に配置する */
    lv_obj_align(BUTTON2, NULL, LV_ALIGN_CENTER, 0, 40);
    /* toggleを有効にする(初期状態は無効) */
    lv_btn_set_checkable(BUTTON2, true);
    /* toggle(初期状態はoffなのでonになる) */
    lv_btn_toggle(BUTTON2);
    /* 自動整形 */
    lv_btn_set_fit2(BUTTON2, LV_FIT_NONE, LV_FIT_TIGHT);
    /* ボタン2の文字 */
    LABEL_2 = lv_label_create(BUTTON2, NULL);
    /* "Toggled"と設定する */
    lv_label_set_text(LABEL_2, "Toggled");
}

void set_label() {
    /* groupの状態を表示するlabel */
    LABEL_GROUPS_MODE = lv_label_create(lv_scr_act(), NULL);
    /* 画面下部中央に配置する */
    lv_obj_align(LABEL_GROUPS_MODE, NULL, LV_ALIGN_IN_BOTTOM_MID, 0, 0);
    /* 文字を表示しない */
    lv_label_set_text(LABEL_GROUPS_MODE, "");
}
void update_label() {
    /*
    GROUP_JOY_SWITCHがEdit modeならばtrue
    Navigate modeならばfalse
    */
    if (lv_group_get_editing(GROUP_JOY_SWITCH)) {
        lv_label_set_text(LABEL_GROUPS_MODE, "Edit mode");
    } else {
        lv_label_set_text(LABEL_GROUPS_MODE, "Navigate mode");
    }
}

void set_groups() {
    /* Wio Terminalの5-wayスイッチ(joy switch)の入力を受けるグループ */
    GROUP_JOY_SWITCH = lv_group_create();
    /* GROUP_JOY_SWITCHにINPUT_DEVICE_JOY_SWITCHを追加する */
    lv_indev_set_group(INPUT_DEVICE_JOY_SWITCH, GROUP_JOY_SWITCH);
    /* GROUP_JOY_SWITCHにBUTTON1を追加する */
    lv_group_add_obj(GROUP_JOY_SWITCH, BUTTON1);
    /* GROUP_JOY_SWITCHにBUTTON2を追加する */
    lv_group_add_obj(GROUP_JOY_SWITCH, BUTTON2);
}

void set_contents() {
    set_buttons();
    set_groups();
    set_label();
}

void setup() {
    /* 設定 */
    backend_setup();

    /* 画面構成 */
    set_contents();
}

void loop() {
    /* タスクを実行した後、待機すべき時間を受け取る */
    uint32_t time_till_next = lv_task_handler();
    /* label更新 */
    update_label();
    /* その時間待機する */
    delay(time_till_next);
    /* 経過した時間をシステムに報せる */
    lv_tick_inc(time_till_next);
}

simple_button_settings.ino
/*
Display flushing
画面いっぱい色で塗り潰している
*/
void display_flush(
    lv_disp_drv_t*      disp,
    const lv_area_t*    area,
    lv_color_t*         color_p
) {
    uint32_t w = (area->x2 - area->x1 + 1);
    uint32_t h = (area->y2 - area->y1 + 1);

    TFT.startWrite();
    TFT.setAddrWindow(area->x1, area->y1, w, h);
    TFT.pushColors(&color_p->full, w * h, true);
    TFT.endWrite();

    /* callbackではこれを召喚しなければならない */
    lv_disp_flush_ready(disp);
}

/* Reading input device */
bool read_encoder(
    lv_indev_drv_t*     indev,
    lv_indev_data_t*    data
) {
    /*
    Wio Terminalの5-wayスイッチ(joy switch)
    press: LV_INDEV_STATE_PR
    release: LV_INDEV_STATE_REL
    */
    if (digitalRead(WIO_5S_PRESS) == LOW) {
        data -> state = LV_INDEV_STATE_PR;
    } else {
        data -> state = LV_INDEV_STATE_REL;
    }

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

    /* no data buffered */
    return false;
}

/* 何もしない */
void event_handler(lv_obj_t * obj, lv_event_t event) {}

void backend_setup() {
    /* lvの初期化 */
    lv_init();

    /* TFT init */
    TFT.begin();
    TFT.setRotation(3);

    /* 画面表示用bufferそのものの初期化 */
    lv_disp_buf_init(&DISP_BUF, COLOR_BUF, NULL, LV_HOR_RES_MAX * 10);

    /* 画面制御設定 */
    lv_disp_drv_t disp_drv;
    lv_disp_drv_init(&disp_drv);
    disp_drv.hor_res    = LV_HOR_RES_MAX;
    disp_drv.ver_res    = LV_VER_RES_MAX;
    disp_drv.flush_cb   = display_flush;
    disp_drv.buffer     = &DISP_BUF;
    lv_disp_t* pDisplay = lv_disp_drv_register(&disp_drv);

    /* 入力の設定 */
    lv_indev_drv_init(&INPUT_DEVICE_DRIVER_JOY_SWITCH);
    INPUT_DEVICE_DRIVER_JOY_SWITCH.type = LV_INDEV_TYPE_ENCODER;
    INPUT_DEVICE_DRIVER_JOY_SWITCH.read_cb = read_encoder;
    INPUT_DEVICE_JOY_SWITCH = lv_indev_drv_register(&INPUT_DEVICE_DRIVER_JOY_SWITCH);

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

概説

プログラムに関して少し説明を付する。

ボタン操作

以前の記事では、物理的な入力を画面への仮想的な入力に変換するため、LV_KEY_*を送るという手法を用いていた。

https://zenn.dev/amenaruya/articles/2df30c285b4d4a
https://zenn.dev/amenaruya/articles/75747f033df51f

グループに対してLV_KEY_*という謂わば仮想的な入力を送ることで、グループに所属する要素が規定の挙動を呈する。

LV_KEY_*

ボタンについてもこのような実装は存在する。

https://github.com/Seeed-Studio/Seeed_Arduino_LvGL/blob/master/src/src/lv_widgets/lv_btn.c#L290-L311

概ね下表のように定められている。

LV_KEY_* onon/offoff
LV_KEY_RIGHT
LV_KEY_UP
onon
LV_KEY_LEFT
LV_KEY_DOWN
offoff

しかしプログラム中のBUTTON1には、実装があるはずのこれが全く効かない。その理由はここにある。

if(lv_btn_get_checkable(btn)) {
    /* ⋯ */
}

若しlv_btn_set_checkable(BUTTON1, true);という記述を加えれば、lv_btn_get_checkable()が真となる。従って、表のような挙動がBUTTON1に見られるようになる。

LV_INDEV_STATE_*

LV_KEY_*でも操作はできるが、これでは「onon/offoff」を切り替えるボタンしか使えないため、端からスイッチ(LvGLLvGLの方)を使えばよい。「押せば押され、離せば戻る」というボタンを使うには、別の手法を用いることとなる。

https://github.com/Seeed-Studio/Seeed_Arduino_LvGL/blob/master/src/src/lv_core/lv_indev.c#L580-L728

https://github.com/Seeed-Studio/Seeed_Arduino_LvGL/blob/master/src/src/lv_hal/lv_hal_indev.h#L49-L51

indev_encoder_proc()には押下の状態LV_INDEV_STATE_PRLV_INDEV_STATE_RELによる条件分岐が複数存在する。この実装を利用することでもボタンへの入力は可能であるが、そのためにはdataという仮引数が既に孰れかの状態を保持している必要がある。

encoderについて

LvGLLvGLには、入力デバイスが複数想定されている。

https://docs.lvgl.io/7.11/porting/indev.html

最も一般的な用途はWioWio TerminalTerminalのような唯の画面を用いるものでなく、タッチスクリーンを用いるものである。また、キーボードも使えるという。このように入力デバイスは環境によって異なるため、大まかな分類が定められている。

  • LV_INDEV_TYPE_POINTER
    タッチスクリーン、マウスなど
  • LV_INDEV_TYPE_KEYPAD
    キーボードなど
  • LV_INDEV_TYPE_ENCODER
    四方入力と押下ができるもの(WioWio TerminalTerminal55-WayWay SwitchSwitchはこれに該当する)
  • LV_INDEV_TYPE_BUTTON
    ボタン(タッチの代替)

本記事ではLV_INDEV_TYPE_ENCODERを想定しているため、それ以外の話は扱っていない。

indev_encoder_proc()を呼び出す箇所は次にある。ここでindev_encoder_proc()dataを渡している。

https://github.com/Seeed-Studio/Seeed_Arduino_LvGL/blob/master/src/src/lv_core/lv_indev.c#L71-L130

但しdataindev_encoder_proc()に渡す前に、_lv_indev_read()に渡している。ここでdataLV_INDEV_STATE_PRLV_INDEV_STATE_RELかの孰れかが保存されている。

https://github.com/Seeed-Studio/Seeed_Arduino_LvGL/blob/master/src/src/lv_hal/lv_hal_indev.c#L124-L157

_lv_indev_read()からは更にindev->driver.read_cb()へ渡されている。これはどこかに定義されているものではなく、吾々が任意に定義するread_cbを指している。

該当箇所抜粋
/* input device driver */
static lv_indev_drv_t   INPUT_DEVICE_DRIVER_JOY_SWITCH;
/* input device */
static lv_indev_t*      INPUT_DEVICE_JOY_SWITCH = nullptr;

/* read_cb */
bool read_encoder(
    lv_indev_drv_t*     indev,
    lv_indev_data_t*    data
) {
    /*
    Wio Terminalの5-wayスイッチ(joy switch)
    press: LV_INDEV_STATE_PR
    release: LV_INDEV_STATE_REL
    */
    if (digitalRead(WIO_5S_PRESS) == LOW) {
        data -> state = LV_INDEV_STATE_PR;
    } else {
        data -> state = LV_INDEV_STATE_REL;
    }}

/* input device driver初期化 */
lv_indev_drv_init(&INPUT_DEVICE_DRIVER_JOY_SWITCH);
/* encoderに設定 */
INPUT_DEVICE_DRIVER_JOY_SWITCH.type = LV_INDEV_TYPE_ENCODER;
/* read_cbを設定 */
INPUT_DEVICE_DRIVER_JOY_SWITCH.read_cb = read_encoder;
/* input deviceにdriverを設定する */
INPUT_DEVICE_JOY_SWITCH = lv_indev_drv_register(&INPUT_DEVICE_DRIVER_JOY_SWITCH);

特に、ボタンへの入力に関わるのは次。

data -> state = LV_INDEV_STATE_PR;

data -> state = LV_INDEV_STATE_REL;

このようにread_cb内でボタン押下の状態を報せる仕組みを定義することで、ボタンへの入力が可能になる。

最後に、グループとして「入力デバイス」と「操作対象」を纏めなければならない。

該当箇所抜粋
/* input device */
static lv_indev_t*      INPUT_DEVICE_JOY_SWITCH = nullptr;
/* joy switch group */
static lv_group_t*      GROUP_JOY_SWITCH = nullptr;

static lv_obj_t*        BUTTON1 = nullptr;
static lv_obj_t*        BUTTON2 = nullptr;

/* Wio Terminalの5-wayスイッチ(joy switch)の入力を受けるグループ */
GROUP_JOY_SWITCH = lv_group_create();
/* GROUP_JOY_SWITCHにINPUT_DEVICE_JOY_SWITCHを追加する */
lv_indev_set_group(INPUT_DEVICE_JOY_SWITCH, GROUP_JOY_SWITCH);
/* GROUP_JOY_SWITCHにBUTTON1を追加する */
lv_group_add_obj(GROUP_JOY_SWITCH, BUTTON1);
/* GROUP_JOY_SWITCHにBUTTON2を追加する */
lv_group_add_obj(GROUP_JOY_SWITCH, BUTTON2);

フォーカス

プログラムを見ると、画面内には二つのボタン(BUTTON1BUTTON2)を配置している。しかし始めはBUTTON1にしか入力することができないため、フォーカスを使って入力対象をBUTTON2に切り替える必要がある。

切り替えの仕組みはindev_encoder_proc()にて既に定義されている。

https://github.com/Seeed-Studio/Seeed_Arduino_LvGL/blob/master/src/src/lv_core/lv_indev.c#L622-L631

仕組みを下表に纏める。

data->enc_diff フォーカス
data->enc_diff < 0
負の値
lv_group_focus_prev(g)
戻る
data->enc_diff > 0
正の値
lv_group_focus_next(g)
進む

先と同様、これもread_cbで吾々が記述する必要がある。

bool read_encoder(
    lv_indev_drv_t*     indev,
    lv_indev_data_t*    data
) {/*
    indev_encoder_proc()に基づく左右の定義
    data -> enc_diff < 0: 左
    data -> enc_diff > 0: 右
    */
    if (digitalRead(WIO_5S_LEFT) == LOW) {
        data -> enc_diff = -1;
    } else if (digitalRead(WIO_5S_RIGHT) == LOW) {
        data -> enc_diff = 1;
    } else {
        data -> enc_diff = 0;
    }

    /* no data buffered */
    return false;
}

ここではスイッチの左右操作をdata -> enc_diffに対応付けることで、ボタンのフォーカス移動が可能になる。ただしフォーカスは、グループがNavigateNavigate modemodeの場合にできる操作である。EditEdit modemodeの場合はLV_KEY_LEFTLV_KEY_RIGHTに対応する。

なおreturn false;とあるが、これには特段の理由がない。このようにread_cbから真理値を返す定義は、最新のLvGLLvGLでは廃止されている。またtrueを返している例を探すことができず、どのように使うものであるか未だ分かっていないが、既に廃止されているのだから無視して良いだろう。

Discussion

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