ESP32 2432S028RでLVGL - ExplorerやFinderの様にフォルダを折り畳める UI の実装

「CYD で MP3 プレイヤ」の続きです 🙂
例えば「夜、寝ながら聴きたいリスト」とか「気分を上げたい時に聴くリスト」など、SD カードに収録されたアルバムから幾つかのパターンに分けたプレイリストを作成する機能を作りたいと思っています。
Windows のエクスプローラーや Mac のファインダーのように一覧でタイトルを見渡せて、フォルダが折り畳め、また複数のタイトルが選択可能な UI の実装が今回のお題です(図1)。
ウィジェットの選定
数ある LVGL のウィジェットから、使えそうなモノをピックアップしてみました。
File Explorer (lv_file_explorer)

lv_file_explorer は、ファイルシステムの巡回に適したウィジェットです。クイックアクセス用の Table (lv_table) と、一覧表示用の List (lv_list) の2ペインの構成が可能ですが、画面の小さな CYD ではレイアウトが苦しそうです。
また LVGL の抽象化ファイルシステム に合わせた SD カード用 FatFs ドライバの作成が必要です。
Menu (lv_menu)

lv_menu ウィジェットは、Label (lv_label)、Button (lv_button)、Image (lv_image) を組み合わせたメニュー用のリストが作成できます。
前述の File Explorer 同様、デフォルトではクリック時の動作が別ページへの遷移となっていますが、この動作を「折り畳み」に変えれば目的の UI が作れるかもしれません。
Table (lv_table)

lv_table は、n行m列のテーブルを作成するウィジェットで、テキスト文字列のみを扱うため軽量さが売りです。
トグルスイッチで行を選択する例題が載っています。フォルダ階層ごとの段付けや「折り畳み」ができるか、要確認です 🤔 今回の第1候補ですネ。
List (lv_list)

lv_list は、前述の Menu 同様、3つのウィジェットを組み合わせたリストが作れるウィジェットです。
Animation (lv_anim) でクリック時の「折り畳み」が実現できそうなので、今回の第2候補としました 😊
プレイリストとしての検証
今回の検証に使用したソフトウェアは次の通りです。
ソフトウェア コンポーネント | バージョン |
---|---|
Arduino IDE | Version 2.3.4 *1 |
esp32 by Espressif Systems | v3.2.1 または v3.3.0 |
ボードタイプ | ESP32 Dev Module または ESP32-2432S028R CYD |
LVGL | v9.2.2 または v9.3.0 |
LovyanGFX | 1.2.7 |
SDライブラリ | SD または SdFat 2.3.0 |
結論から言えば、第1候補も第2候補も「帯に短し襷に長し」でしたので、両者の「いいとこ取り」をした改善案を作成しています。ですが、まずはそれぞれの特徴を検証した結果を報告したいと思います。
LVGL 9.3.0 の警告について
コンパイル時に次の警告が出ます(パスは Mac の場合)。
/Users/<user>/Library/Arduino15/packages/esp32/tools/esp-x32/2411/bin/../lib/gcc/xtensa-esp-elf/14.2.0/../../../../xtensa-esp-elf/bin/ld: warning: /Users/<user>/Library/Arduino15/packages/esp32/tools/esp-x32/2411/bin/../lib/gcc/xtensa-esp-elf/14.2.0/esp32/no-rtti/crtn.o: missing .note.GNU-stack section implies executable stack
/Users/<user>/Library/Arduino15/packages/esp32/tools/esp-x32/2411/bin/../lib/gcc/xtensa-esp-elf/14.2.0/../../../../xtensa-esp-elf/bin/ld: NOTE: This behaviour is deprecated and will be removed in a future version of the linker
“missing .note.GNU-stack section implies executable stack” を調べると、実行可能なスタック領域にアセンブラコードが配置されるような場合に、「バッファオーバーフロー などの脆弱性に気をつけろ」ということのようです。
LVGL 9.3.0 との関連性は不明ですが、ESP32-IDF の基本コンポーネント newlib-esp32 の crtn.c
や crtn.S
には、確かにアセンブラコードが含まれています。
まぁ、GitHub のセキュリティ は時々覗くとして、趣味程度のアプリなら気にしない、と 高を括ってます 🙄
第1候補の Table (lv_table) で作ってみる
例題 Lightweighted list from table(図2)のコードを元に、徐々にスタイルを調整し、まずは 図3 のような見た目にしてみました。
lv_table
の特徴は、テーブルのセル中にテキストを表示するだけの機能に特化していて、とても「軽量」なことです。200 行の表の作成に数十 ms しかかかりません。注意すべきは、予め作成する表の行数と列数を指定しておくことです。指定せずとも作成は可能ですが、図2 の例では約 1.5 秒かかりました 😅
また 図2 のトグルスイッチは、lv_switch
のインスタンスを 200 個生成する代わりに、長方形と正方形に角丸を指定した長円と真円を各セルに直接描画しています。そのコードを改修し、チェックボックスに置き換えてみたのが 図3 です。



さらに目標とするイメージに無理矢理に近付けたのが 図4 です。この段階で分かったことは、lv_table
の内部には lv_anim
が適用できるウィジェットが存在せず、アニメーションによる折り畳みの実現は無理っポイということでした 🥺
裏を返せば、このシンプルさが lv_table
が軽量な理由です 💪
第2候補の List (lv_list) で作ってみる

超シンプルな lv_table
とは異なり、lv_list
は3つのウィジェット lv_label
、lv_button
、lv_image
を内包するコンテナウィジェットです(図5)。
例題の Simple List(図6)を元に、少しずつスタイルを調整しながら目的の UI を作成してみました(図7、図8)。各行の lv_button
にアニメーションが適用できるので、フォルダ単位の折り畳みもバッチリです 👍



ウィジェットツリーの確認方法
lv_obj_dump_tree(lv_obj_t *start_obj)
で start_obj
(NULL
の場合は、表示中のスクリーン)を親とするツリーを LVGL のログ(通常はシリアルモニタ)に出力することができます。この関数を使うには、lv_conf.h
の下記シンボルを有効にします。
/** Enable log module */
#define LV_USE_LOG 1
#if LV_USE_LOG
/** Set value to one of the following levels of logging detail:
* - LV_LOG_LEVEL_TRACE Log detailed information.
* - LV_LOG_LEVEL_INFO Log important events.
* - LV_LOG_LEVEL_WARN Log if something unwanted happened but didn't cause a problem.
* - LV_LOG_LEVEL_ERROR Log only critical issues, when system may fail.
* - LV_LOG_LEVEL_USER Log only custom log messages added by the user.
* - LV_LOG_LEVEL_NONE Do not log anything. */
#define LV_LOG_LEVEL LV_LOG_LEVEL_WARN // または LV_LOG_LEVEL_USER
/** Add `id` field to `lv_obj_t` */
#define LV_USE_OBJ_ID 1 // 0 の場合、オブジェクトを識別する ID が全て 0 になる
/** Use builtin obj ID handler functions:
* - lv_obj_assign_id: Called when a widget is created. Use a separate counter for each widget class as an ID.
* - lv_obj_id_compare: Compare the ID to decide if it matches with a requested value.
* - lv_obj_stringify_id: Return string-ified identifier, e.g. "button3".
* - lv_obj_free_id: Does nothing, as there is no memory allocation for the ID.
* When disabled these functions needs to be implemented by the user.*/
#define LV_USE_OBJ_ID_BUILTIN 1
詳しくは「Dumping a Widget Tree」を参照してください。
ちょっとした問題点としては、SD ライブラリではフォルダやファイルのリスティング順が制御できないことです(図9)。そこで前回記事「ESP32 2432S028RとLVGLでMP3 Player - N分木(N-ary Tree)によるプレイリストの実装」で作成したコードを利用し、アルファベット順に表示するプログラムも作成しました(図10)。



最も大きな問題点としては、メモリ消費量が多いということです(図11)。今回は楽曲単位ではなくアルバム単位で選択できる UI の作成が目標ですが、このままでは実質 320KB の SRAM で数十枚程度のアルバムしか管理できない見通しになります 😣
省メモリ版の検討
そこで「折り畳み」と「メモリ消費量の削減」を両立させる案として、第2候補に第1候補のテクニックを適用することを考えました。
lv_list
をベースに、メモリを喰うlv_button
+lv_label
+lv_image
は使わない- 代わりにセルの追加は、
lv_anim
でアニメーションも適用できるlv_label
で代行する - チェックボックスなどのアイコン画像は、
lv_table
と同様のテクニックでレンダリング - Long modes は、
LV_LABEL_LONG_SCROLL_CIRCULAR
→LV_LABEL_LONG_CLIP
に変更
この方針を元に作成したのが 図12 です。図4 の lv_table
ほどではありませんが、図8 に比べてメモリ消費量を半減できました 👍

折り畳みの仕組み
lv_list
にセルを追加する際、「ディレクトリ階層の深さ」に加え、「折り畳み」と「選択」の状態を表す情報を付加しています。
// 各セルに付加する情報
typedef union {
void *user_data; // lv_obj_{set|get}_user_data() への引数
struct {
uint8_t key; // 識別用のキー
uint8_t depth; // 階層の深さ(ルートは深さゼロ)
uint8_t type; // 0: フォルダ、1: 末端のフォルダ/ファイル
bool status; // true: 折り畳み状態(フォルダの場合)、選択済み状態(末端の場合)
};
} CellData_t;

あるセルがクリックされた時にこの情報を参照し、自身より「階層の深さ」が大きいセルを折り畳みの対象とします。
lv_list
中のウィジェットは関数 lv_obj_get_sibling()
で辿れるので(図13)、高さを変えるアニメーションを設定することで「折り畳み」を実現しています。
この方法のイケてない点は、折り畳み対象のセル数に比例してアニメーションに必要なメモリが増大することです。

そこで、折り畳み対象のセルを一まとめにするため、乱暴にも lv_list
や lv_label
でラップしてみました(図14)。が、残念ながら3階層目以降のセルが正しく表示されませんでした。やはり乱暴はイケマセンね 💩
省メモリ版のプログラムについて
プログラムは CYD + LVGL のガラクタ置き場 に上げてあります。
LVGL_Arduino_FileList
├── LVGL_Arduino_FileList.ino
├── list.cpp // リスト構築用のコード
├── list.h // 〃
├── sdspi.h // SDカードの設定
├── src // 日本語フォント、アイコン画像など
├── tree.cpp // N分木構築用のコード
└── tree.hpp // 〃
LVGL_Arduino_FileList.ino
の前半は LovyanGFX と LVGL の設定です。「SD カード直読み版」と「ファイルツリー構築版」を切り替えは、setup()
の最後で行います。
#if 0
/* SD カード直読み版 */
add_list(dir, list, 2); // 2: 2階層まで or 3: 3階層まで("ROOT_FOLDER"を除く)
#else
/* ファイルツリー構築版 */
Node *root = new Node(ROOT_FOLDER);
root->scan_dir(dir); // 末端のディレクトリを選択対象とする
//root->scan_file(dir); // 末端のファイルを選択対象とする
add_list(root, list);
delete root;
#endif
また ESP32 標準の SD ライブラリと SdFat の切り替えは、spdi.h
で行います。
// #define USE_SDFAT
#ifdef USE_SDFAT
//--------------------------------------------------------------------
// SdFat library
// https://github.com/greiman/SdFat
//--------------------------------------------------------------------
#include "SdFat.h"
メモリ消費量について
削減したとは言え、アプリケーションにとっては相変わらず厳しい状況です。
LVGL では、lv_conf.h
で設定された LV_MEM_SIZE
が足りなくなるとアサーションが実行され(V_USE_ASSERT_*
の設定次第)、また try - catch
ステートメントも効かないため、アプリケーション側でサイズを超えないように管理する必要があります。
このため生成するセル数を list.h
の設定で制限しています。
#define MAX_CELLS 100 // LV_MEM_SIZE = (64 * 1024U)

また今回の実装は、言ってみればトポロジー的には同じツリー構造の表現形式を変換したに過ぎず、中身のテキストデータが重複しているという無駄があります。イザとなれば、この辺も要改善項目かなぁと思っています。
Arduino の ESP32 で try() - catch
が効かない理由
Google にその理由を聞いたところ、AI から次の回答が得られました。
ESP32開発において、Arduino IDEでtry-catch文を直接使用できない主な理由は、コードサイズとリソース消費を抑えるため、Arduinoビルド環境ではC++例外処理がデフォルトで無効になっていることが多いためです。 ESP32マイクロコントローラ自体はC++例外処理が可能ですが、標準のArduinoフレームワークと、ESP32を含む様々なボード用の関連ツールチェーンでは、多くの場合、-fno-exceptionsフラグを使用してコードをコンパイルします。このフラグは、try-catchメカニズムを含む例外処理を明示的に無効にします。
出典は不明ですが、コア系の再コンパイルが必要なようです。
さて…
16GB のμSD カードに、理想は1500曲程度、目標は1000曲以上が管理出来ることが目標です。
何度か心が折れそうになりながらも目標に到達できそうな気配が感じられるまで、あと一歩というところまで来ました。
もうひと頑張りします 🥳
-
「After exiting sleep mode, IDE reloads and changes are lost #2704」が未解決のため、2.3.4 に留まっています。 ↩