【Wio Terminal/LvGL】Button and focus handling using a encoder
スイッチ(物理)でボタン(画面内)操作とフォーカス
ボタンを押すだけのことだが、案外苦労した。
引用:https://www.switch-science.com/products/6360
プログラム
概形はこの例を参考にした。動作はほとんどこの通りのため、実行の様子は省略している。
#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);
}
/*
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_*
を送るという手法を用いていた。
グループに対してLV_KEY_*
という謂わば仮想的な入力を送ることで、グループに所属する要素が規定の挙動を呈する。
LV_KEY_*
ボタンについてもこのような実装は存在する。
概ね下表のように定められている。
LV_KEY_* |
|
---|---|
LV_KEY_RIGHT LV_KEY_UP
|
|
LV_KEY_LEFT LV_KEY_DOWN
|
しかしプログラム中のBUTTON1
には、実装があるはずのこれが全く効かない。その理由はここにある。
if(lv_btn_get_checkable(btn)) {
/* ⋯ */
}
若しlv_btn_set_checkable(BUTTON1, true);
という記述を加えれば、lv_btn_get_checkable()
が真となる。従って、表のような挙動がBUTTON1
に見られるようになる。
LV_INDEV_STATE_*
LV_KEY_*
でも操作はできるが、これでは「
indev_encoder_proc()
には押下の状態LV_INDEV_STATE_PR
とLV_INDEV_STATE_REL
による条件分岐が複数存在する。この実装を利用することでもボタンへの入力は可能であるが、そのためにはdata
という仮引数が既に孰れかの状態を保持している必要がある。
encoderについて
最も一般的な用途は
-
LV_INDEV_TYPE_POINTER
タッチスクリーン、マウスなど -
LV_INDEV_TYPE_KEYPAD
キーボードなど -
LV_INDEV_TYPE_ENCODER
四方入力と押下ができるもの(の - はこれに該当する) -
LV_INDEV_TYPE_BUTTON
ボタン(タッチの代替)
本記事ではLV_INDEV_TYPE_ENCODER
を想定しているため、それ以外の話は扱っていない。
indev_encoder_proc()
を呼び出す箇所は次にある。ここでindev_encoder_proc()
にdata
を渡している。
但しdata
をindev_encoder_proc()
に渡す前に、_lv_indev_read()
に渡している。ここでdata
にLV_INDEV_STATE_PR
かLV_INDEV_STATE_REL
かの孰れかが保存されている。
_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);
フォーカス
プログラムを見ると、画面内には二つのボタン(BUTTON1
とBUTTON2
)を配置している。しかし始めはBUTTON1
にしか入力することができないため、フォーカスを使って入力対象をBUTTON2
に切り替える必要がある。
切り替えの仕組みはindev_encoder_proc()
にて既に定義されている。
仕組みを下表に纏める。
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
に対応付けることで、ボタンのフォーカス移動が可能になる。ただしフォーカスは、グループがLV_KEY_LEFT
とLV_KEY_RIGHT
に対応する。
なおreturn false;
とあるが、これには特段の理由がない。このようにread_cb
から真理値を返す定義は、最新のtrue
を返している例を探すことができず、どのように使うものであるか未だ分かっていないが、既に廃止されているのだから無視して良いだろう。
Discussion