Raspberry Pi + Yocto + LVGLで簡単なUIの実装
作りたいものの全体像から参照しています
作りたいもの
Raspberry Pi 4 (RasPi4)にタッチパネル付きLCDをつけて、簡単なUIを製作します。
基本的には他のアプリケーションからメッセージを受けてビジュアライズ + 安全なRasPi4のシャットダウンのみのUIを実装します。メッセージングにはMQTTを使用します。
動作する全ソースコードをGitHubで公開しています。
ハードウェア
- RasPi4
- タッチパネル付きLCD
- ELECROWというメーカーの5インチLCD、静電容量式タッチパネル
- LCDのインタフェースはHDMI
- タッチパネルのインタフェースはUSB
- Amazonで買えます
デバイスドライバ
デバイスドライバはYocto 5.2のRasPi4ターゲット標準のデバイスドライバを使用します。
ただし800x480解像度は標準のデバイスドライバでは対応していないため、ピクセル2ピクセルで表示するにはKernelのブートパラメータに追加が必要です。詳細はこちら。
タッチパネルはUSBのHIDとして見えます。
メッセージング
メッセージングにはMQTTを使用します。MQTTブローカーを立てて、UIアプリはそこにsubscribeすることでメッセージを取得します。MQTTブローカーはmosquitto、クライアントはPaho-mqtt-cppを使用します。
LVGLインテグレーション
LVGLというCライブラリを使用して簡単なUIを実現します。以前使ったことがあるバージョンのv8.3.10を使います。(このバージョンに対応するlv_driversはアーカイブされているので、バージョンアップを検討した方が良さそうです)
処理の流れの概要はこのようになります。
-
lv_init
でLVGLの初期化 -
fbdev_init
でfbデバイスの初期化、fbdevからLCDピクセルサイズを取得し、ハードウェア非依存のdisplay定義であるlv_disp_drv_t
を設定してlv_disp_drv_register
で登録 -
evdev_init
でeventデバイスの初期化、ハードウェア非依存のinput device定義であるlv_indev_drv_t
を設定してlv_indev_drv_register
で登録 - アプリケーションはLVGLの高レベルAPIでUIコンポーネントを配置
- アプリケーションは一定周期で
lv_tick_inc
を呼び、経過時間(実時間の増分)をLVGLに通知、またlv_task_handler
を呼び、入力イベントをハンドリングさせる - アプリケーションからのUIコンポーネントの変更、また入力イベントのハンドルによってUIが変更された場合、LVGLはHAL層に描画
- VSYNC毎にHAL層がdirtyである場合はfbデバイスのフレームバッファにコピーし出力される
UI
画面遷移のない、1画面のUIを作ります。
実装
それでは作っていきます。meta-sample-uiレポジトリを作成し、その下にrecipes-sample-uiディレクトリを作成してここにレシピを作っていきます。なお、initマネージャはsystemdを前提にしています。
MQTT関連
実際のレシピはこちらです。
MQTTブローカー・クライアントライブラリのインストール
MQTTブローカーとMQTTクライアントライブラリをインストールするため、build/conf/local.conf
に以下を追加しています。
CORE_IMAGE_EXTRA_INSTALL:append = " mosquitto"
CORE_IMAGE_EXTRA_INSTALL:append = " mosquitto-clients"
CORE_IMAGE_EXTRA_INSTALL:append = " paho-mqtt-c"
CORE_IMAGE_EXTRA_INSTALL:append = " paho-mqtt-cpp"
mosquittoはディフォルトではlocalhostのみ通信可能なため、設定を変更します。そのためのレシピをmosquitto-customとして以下のように作成します。
recipes-sample-ui +- mosquitto-custom +- files +- mosquitto-sample.conf
| +- mosquitto-sample.service
+- mosquitto-autorun.bb
+- mosquitto_2.0.20.bbappend
まず、ディフォルトのmosquittoの自動起動をキャンセルします。ディフォルトのmosquittoのレシピはmeta-openembedded/meta-networking/recipes-connectivity/mosquitto/にあります。同名のbbappendファイルを作成し、自動起動をキャンセルします。
SYSTEMD_AUTO_ENABLE:${PN} = "disable"
その上で、変更したconfファイルを使ったmosquittoを自動起動します。confの変更点は以下の通りです。セキュリティは考慮していません。
allow_anonymous true →無名クライアントを許可
(snip)
listener 1883 →すべてのインタフェースでポート1883で待ち受け
UI本体
実際のレシピはこちらです。
ソースコードの準備
まずプロジェクト用のディレクトリを以下のように作ります。私の場合、あまりUIをいじる気がないのでレシピのfilesディレクトリに直接ファイルを置いていますが、UIをいじっていく場合は、UIのレポジトリを分けた方がいいかもしれません。
<root> +- src +- lvgl/
| +- lv_drivers/
| +- lv_conf.h
| +- lv_drv_conf.h
| +- main.cpp
| ...
+- CMakeLists.txt
lvglとlv_driversをcloneします。
cd src
git clone -b v8.3.10 https://github.com/lvgl/lvgl.git
git clone -b v8.3.0 https://github.com/lvgl/lv_drivers.git
またはこのプロジェクトをgitで管理している場合はsubmoduleで追加します。
cd src
git submodule add https://github.com/lvgl/lvgl.git lvgl
cd lvgl
git checkout v8.3.10
cd ..
git submodule add https://github.com/lvgl/lv_drivers.git lv_drivers
cd lv_drivers
git checkout v8.3.0
cd ..
lvglおよびlv_driversからconfigヘッダをコピーします。
cp lvgl/lv_conf_template.h ./lv_conf.h
cp lv_drivers/lv_drv_conf_template.h ./lv_drv_conf.h
lv_conf.h
文字を表示するためフォントデータをリンクします。
#define LV_FONT_MONTSERRAT_24 1
lv_drv_conf.h
まずヘッダ全体をenableします。
#if 1 /*Set it to "1" to enable the content*/
fbデバイスを使用します。デバイスはRasPi4のtype-c側のHDMIを使用するので/dev/fb0
のままでOKです。
/*-----------------------------------------
* Linux frame buffer device (/dev/fbx)
*-----------------------------------------*/
#ifndef USE_FBDEV
# define USE_FBDEV 1
#endif
#if USE_FBDEV
# define FBDEV_PATH "/dev/fb0"
#endif
eventデバイスを使用します。デバイスはudevルールで/dev/input/touchscreen0
にエイリアスを作るのでこれを指定します。
/*-------------------------------------------------
* Mouse or touchpad as evdev interface (for Linux based systems)
*------------------------------------------------*/
#ifndef USE_EVDEV
# define USE_EVDEV 1
#endif
#ifndef USE_BSD_EVDEV
# define USE_BSD_EVDEV 0
#endif
#if USE_EVDEV || USE_BSD_EVDEV
# define EVDEV_NAME "/dev/input/touchscreen0" /*You can use the "evtest" Linux tool to get the list of devices and test them*/
# define EVDEV_SWAP_AXES 0 /*Swap the x and y axes of the touchscreen*/
# define EVDEV_CALIBRATE 1 /*Scale and offset the touchscreen coordinates by using maximum and minimum values for each axis*/
# if EVDEV_CALIBRATE
# define EVDEV_HOR_MIN 0 /*to invert axis swap EVDEV_XXX_MIN by EVDEV_XXX_MAX*/
# define EVDEV_HOR_MAX 800 /*"evtest" Linux tool can help to get the correct calibraion values>*/
# define EVDEV_VER_MIN 0
# define EVDEV_VER_MAX 480
# endif /*EVDEV_CALIBRATE*/
#endif /*USE_EVDEV*/
mainとその他
おおまかな構成は以下のようになります。詳細はリンク先のコードを参照してください。
- mainから「UIのtickおよびイベントハンドリングループ」「メッセージ受信ループ」の2つのスレッドを作成し、mainはシグナル待ちとなります。
- UIのtickおよびイベントハンドリングループ
- UIのループの開始時に
lv_drivers
およびlvgl
を初期化し、ディフォルトの画面を構成します。 - スレッドコンテキストのループ内で
lv_tick_inc
を呼び経過時間をインクリメント、lv_task_handler
を呼びUIイベントのハンドリングします。 - shutdownボタンが押されると
event_handler
がコールバックされ、ここでは自身のプロセスにSIGINTを通知します。main処理でSIGINTを受信すると終了コード130でプロセスを終了します。その後UIを起動するシェルスクリプトが終了コードを判定してshutdownします。
- UIのループの開始時に
- メッセージ受信ループ
- メッセージ受信ループ開始時にMQTTブローカーに接続して決められたtopicにsubscribeしておきます。
- スレッドコンテキストのループ内で同期的にメッセージを受信し、受信したら登録された
MessageReceivable
インタフェースを持つクラス(ここではDefaultUserInterface
)のOnReceive
に生データを通知します。 -
DefaultUserInterface
はJSON形式のペイロードをパースして現在のViewに値をセットします。
- UIのtickおよびイベントハンドリングループ
※UI更新ループとイベントハンドルループはスレッドを分けたほうがイベントハンドル処理がUI更新に影響しないので良いですが、ここでは簡単のため一つのループ内で行っています。従ってイベントハンドル中はUI更新は止まります。
タッチスクリーンのエイリアス登録
実際のレシピはこちらです。
今回はUSBのVID/PIDで見分けてエイリアスを登録するudevルールを追加します。ベンダIDが0484でプロダクトIDが5750の場合は、/dev/input/event*に加えて/dev/input/touchscreen0というエイリアスを作成しています。
SUBSYSTEM=="input", ATTRS{idVendor}=="0484", ATTRS{idProduct}=="5750", ENV{DEVICE}="PRIMARY_TOUCHSCREEN"
ENV{DEVICE}=="PRIMARY_TOUCHSCREEN", SYMLINK+="input/touchscreen0"
なお、このルールだと同パネルが複数の場合は対応できません。その場合は物理的なUSBのコネクタ(バス番号など)で見分けることができます。
UIの自動起動
実際のレシピはこちらです。
シェルスクリプトを作成し上記のUIアプリを起動、その終了コードが規定のものであればシェルスクリプトからRasPiのシャットダウンを実施します。このシェルスクリプトを自動起動するsystemdサービスを作成します。
Yoctoターゲットのビルド対象への追加
最後にこれまでのレシピをYoctoターゲットのビルド対象に追加します。
${TOPDIR}/../meta-sample-ui \
CORE_IMAGE_EXTRA_INSTALL:append = " sample-ui"
CORE_IMAGE_EXTRA_INSTALL:append = " sample-ui-touchscreen"
CORE_IMAGE_EXTRA_INSTALL:append = " sample-ui-autorun"
CORE_IMAGE_EXTRA_INSTALL:append = " mosquitto-autorun"
Discussion