図1 Macのファインダー
図1 Macのファインダー

CYD で MP3 プレイヤ」の続きです 🙂

例えば「夜、寝ながら聴きたいリスト」とか「気分を上げたい時に聴くリスト」など、SD カードに収録されたアルバムから幾つかのパターンに分けたプレイリストを作成する機能を作りたいと思っています。

Windows のエクスプローラーや Mac のファインダーのように一覧でタイトルを見渡せて、フォルダが折り畳め、また複数のタイトルが選択可能な UI の実装が今回のお題です(図1)。

ウィジェットの選定

数ある LVGL のウィジェットから、使えそうなモノをピックアップしてみました。

File Explorer (lv_file_explorer)

lv_file_explorer
lv_file_explorer

lv_file_explorer は、ファイルシステムの巡回に適したウィジェットです。クイックアクセス用の Table (lv_table) と、一覧表示用の List (lv_list) の2ペインの構成が可能ですが、画面の小さな CYD ではレイアウトが苦しそうです。

また LVGL の抽象化ファイルシステム に合わせた SD カード用 FatFs ドライバの作成が必要です。

lv_menu
lv_menu

lv_menu ウィジェットは、Label (lv_label)Button (lv_button)Image (lv_image) を組み合わせたメニュー用のリストが作成できます。

前述の File Explorer 同様、デフォルトではクリック時の動作が別ページへの遷移となっていますが、この動作を「折り畳み」に変えれば目的の UI が作れるかもしれません。

Table (lv_table)

lv_table
lv_table

lv_table は、n行m列のテーブルを作成するウィジェットで、テキスト文字列のみを扱うため軽量さが売りです。

トグルスイッチで行を選択する例題が載っています。フォルダ階層ごとの段付けや「折り畳み」ができるか、要確認です 🤔 今回の第1候補ですネ。

List (lv_list)

lv_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-esp32crtn.ccrtn.S には、確かにアセンブラコードが含まれています。

まぁ、GitHub のセキュリティ は時々覗くとして、趣味程度のアプリなら気にしない、と 高を括ってます 🙄

第1候補の Table (lv_table) で作ってみる

例題 Lightweighted list from table図2)のコードを元に、徐々にスタイルを調整し、まずは 図3 のような見た目にしてみました。

lv_table の特徴は、テーブルのセル中にテキストを表示するだけの機能に特化していて、とても「軽量」なことです。200 行の表の作成に数十 ms しかかかりません。注意すべきは、予め作成する表の行数と列数を指定しておくことです。指定せずとも作成は可能ですが、図2 の例では約 1.5 秒かかりました 😅

また 図2 のトグルスイッチは、lv_switch のインスタンスを 200 個生成する代わりに、長方形と正方形に角丸を指定した長円と真円を各セルに直接描画しています。そのコードを改修し、チェックボックスに置き換えてみたのが 図3 です。

図2 lv_table の例題
図2 lv_table の例題
図3 lv_table の応用例
図3 lv_table の応用例
図4 折り畳めない!
図4 折り畳めない!

さらに目標とするイメージに無理矢理に近付けたのが 図4 です。この段階で分かったことは、lv_table の内部には lv_anim が適用できるウィジェットが存在せず、アニメーションによる折り畳みの実現は無理っポイということでした 🥺

裏を返せば、このシンプルさが lv_table が軽量な理由です 💪

第2候補の List (lv_list) で作ってみる

図5 lv_list のウィジェットツリー
図5 lv_list のウィジェットツリー

超シンプルな lv_table とは異なり、lv_list は3つのウィジェット lv_labellv_buttonlv_image を内包するコンテナウィジェットです(図5)。

例題の Simple List図6)を元に、少しずつスタイルを調整しながら目的の UI を作成してみました(図7図8)。各行の lv_button にアニメーションが適用できるので、フォルダ単位の折り畳みもバッチリです 👍

図6 lv_list の例題
図6 lv_list の例題
図7 lv_list の応用例
図7 lv_list の応用例
図8 checkbox の追加例
図8 checkbox の追加例
ウィジェットツリーの確認方法 lv_obj_dump_tree(lv_obj_t *start_obj)start_objNULL の場合は、表示中のスクリーン)を親とするツリーを 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)。

図9 SD カード直読み版
図9 SD カード直読み版
図10 ファイルツリー構築版
図10 ファイルツリー構築版
図11 メモリ消費量がヤバい
図11 メモリ消費量がヤバい

最も大きな問題点としては、メモリ消費量が多いということです(図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_CIRCULARLV_LABEL_LONG_CLIP に変更

この方針を元に作成したのが 図12 です。図4lv_table ほどではありませんが、図8 に比べてメモリ消費量を半減できました 👍

図12 省メモリ版
図12 省メモリ版
デモ1(2階層目まで)
デモ2(3階層目まで)

折り畳みの仕組み

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;
図13 ファイル階層の辿り方
図13 ファイル階層の辿り方

あるセルがクリックされた時にこの情報を参照し、自身より「階層の深さ」が大きいセルを折り畳みの対象とします。

lv_list 中のウィジェットは関数 lv_obj_get_sibling() で辿れるので(図13)、高さを変えるアニメーションを設定することで「折り畳み」を実現しています。

この方法のイケてない点は、折り畳み対象のセル数に比例してアニメーションに必要なメモリが増大することです。

図14 lv_list の階層化は失敗!
図14 lv_list の階層化は失敗!

そこで、折り畳み対象のセルを一まとめにするため、乱暴にも lv_listlv_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曲以上が管理出来ることが目標です。

何度か心が折れそうになりながらも目標に到達できそうな気配が感じられるまで、あと一歩というところまで来ました。

もうひと頑張りします 🥳


  1. After exiting sleep mode, IDE reloads and changes are lost #2704」が未解決のため、2.3.4 に留まっています。