😺

Raspberry Pi + Yocto + LVGLで簡単なUIの実装

に公開

作りたいものの全体像から参照しています
https://zenn.dev/takumique/articles/a451ffd2393b1f

作りたいもの

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はアーカイブされているので、バージョンアップを検討した方が良さそうです)

処理の流れの概要はこのようになります。

  1. lv_initでLVGLの初期化
  2. fbdev_initでfbデバイスの初期化、fbdevからLCDピクセルサイズを取得し、ハードウェア非依存のdisplay定義であるlv_disp_drv_tを設定してlv_disp_drv_registerで登録
  3. evdev_initでeventデバイスの初期化、ハードウェア非依存のinput device定義であるlv_indev_drv_tを設定してlv_indev_drv_registerで登録
  4. アプリケーションはLVGLの高レベルAPIでUIコンポーネントを配置
  5. アプリケーションは一定周期でlv_tick_incを呼び、経過時間(実時間の増分)をLVGLに通知、またlv_task_handlerを呼び、入力イベントをハンドリングさせる
  6. アプリケーションからのUIコンポーネントの変更、また入力イベントのハンドルによってUIが変更された場合、LVGLはHAL層に描画
  7. VSYNC毎にHAL層がdirtyである場合はfbデバイスのフレームバッファにコピーし出力される

UI

画面遷移のない、1画面のUIを作ります。

実装

それでは作っていきます。meta-sample-uiレポジトリを作成し、その下にrecipes-sample-uiディレクトリを作成してここにレシピを作っていきます。なお、initマネージャはsystemdを前提にしています。

MQTT関連

実際のレシピはこちらです。

MQTTブローカー・クライアントライブラリのインストール

MQTTブローカーとMQTTクライアントライブラリをインストールするため、build/conf/local.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ファイルを作成し、自動起動をキャンセルします。

mosquitto_2.0.20.bbappend
SYSTEMD_AUTO_ENABLE:${PN} = "disable"

その上で、変更したconfファイルを使ったmosquittoを自動起動します。confの変更点は以下の通りです。セキュリティは考慮していません。

mosquitto-sample.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

文字を表示するためフォントデータをリンクします。

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です。

lv_drv_conf.h
/*-----------------------------------------
 *  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にエイリアスを作るのでこれを指定します。

lv_drv_conf.h
/*-------------------------------------------------
 * 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します。
    • メッセージ受信ループ
      • メッセージ受信ループ開始時にMQTTブローカーに接続して決められたtopicにsubscribeしておきます。
      • スレッドコンテキストのループ内で同期的にメッセージを受信し、受信したら登録されたMessageReceivableインタフェースを持つクラス(ここではDefaultUserInterface)のOnReceiveに生データを通知します。
      • DefaultUserInterfaceはJSON形式のペイロードをパースして現在のViewに値をセットします。

※UI更新ループとイベントハンドルループはスレッドを分けたほうがイベントハンドル処理がUI更新に影響しないので良いですが、ここでは簡単のため一つのループ内で行っています。従ってイベントハンドル中はUI更新は止まります。

タッチスクリーンのエイリアス登録

実際のレシピはこちらです。

今回はUSBのVID/PIDで見分けてエイリアスを登録するudevルールを追加します。ベンダIDが0484でプロダクトIDが5750の場合は、/dev/input/event*に加えて/dev/input/touchscreen0というエイリアスを作成しています。

usb_touchscreen.rules
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ターゲットのビルド対象に追加します。

bblayers.conf
  ${TOPDIR}/../meta-sample-ui \
local.conf
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