Introduction
Les interfaces graphiques jouent un rôle crucial dans le succès et l'adoption d'applications. Une bonne interface peut sublimer une application et largement contribuer à son succès. En revanche, une mauvaise interface peut entraver l'expérience utilisateur et, éventuellement, mener à l'échec de l'application.
Qui n'a jamais été frustré par une application à cause de son design ou de sa lenteur ?
De nombreuses technologies ont été développées pour répondre à ce besoin d'interfaces élégantes et performantes. Ces solutions sont désormais variées, couvrant un grand nombre de langages de programmation et de plateformes (bureau, mobile, embarqué...), afin de satisfaire tous types de besoins. Une panoplie d'outils s'est également créée autour de ces projets pour améliorer l'expérience de développement et faciliter la création d'interfaces utilisateur de qualité.
Cet article a pour but de vous présenter certaines de ces technologies, en mettant l'accent sur leur utilisation dans le développement d'applications embarquées.
⚙️ Technologies étudiées
Les technologies d'interfaces sont variées et il serait impossible de les couvrir toutes en détail. Toutefois, nous en avons sélectionné et étudié quelques-unes qui nous semblaient particulièrement intéressantes.
Ces technologies sont regroupées dans le tableau ci-dessous :
Technologie | Licence | Web |
QML | LGPLv3|GPLv2/3|Commercial | ❌ |
QtWebengine | LGPLv3|Commercial | ✅ |
Gtk | LGPLv2.1 | ❌ |
Tauri | MIT/Apache2.0 | ✅ |
WPE | BSD2 | ✅ |
LVGL | MIT | ❌ |
LVGLJS | MIT | ~ |
Raygui | zlib | ❌ |
Flutter | BSD3 | ❌ |
🌐 Web ou pas web ?
Comme vous pouvez le voir dans le tableau, j'ai fait une distinction entre les technologies se basant sur des langages web et les autres solutions plus classiques.
Actuellement, la tendance en matière de développement d'applications graphiques est l'utilisation de technologies web. Des projets comme Electron ont énormément gagné en popularité et ont contribué au succès d'importantes applications telles que VS Code, Slack et Notion. Cette approche est privilégiée, car elle permet aux entreprises de réduire le temps de développement et de proposer une expérience utilisateur homogène sur les applications de bureau et web.
La question de l'utilisation de ce genre de technologies peut désormais se poser pour les applications embarquées. Cependant, les contraintes matérielles sont plus importantes dans ces environnements. Il est donc important d'évaluer les consommations en ressources ainsi que les performances de ces solutions, avant de considérer leur utilisation et leur déploiement.
📊 Performances
Avant de vous présenter individuellement chaque technologie, je vous propose d'observer les résultats des tests de performances. De cette façon, vous pourrez avoir une première idée des différences entre ces technologies. (Et vous verrez que les technologies web sont envisageables.)
Pour comparer les différentes solutions, nous avons développé des applications se basant sur chacune des technologies sélectionnées. Chacune de ces applications ont intégré une interface similaire. L'objectif était de comparer les différentes technologies dans un contexte précis : les interfaces embarquées de supervision ou de configuration. L'IHM développée était donc assez simple, mais complète, intégrant de nombreux composants graphiques tels que des cases à cocher, des boutons, des animations et des graphiques.
Ici, vous pouvez voir cette interface, divisée en 3 différents onglets :
Cette interface est intégrée sur un petit écran tactile de cinq pouces. Ensuite, un scénario de test a été défini pour interagir avec l'ensemble de l'interface.
Ce scénario a été rejoué cinq fois sur chacune des applications, en mesurant la consommation CPU, l'empreinte mémoire et le nombre d'images par seconde affichées.
Pour réaliser ces tests de performance, plusieurs outils ont été utilisés :
- Libinput pour enregistrer et rejouer les entrées utilisateur.
- Psutil pour mesurer certaines métriques système comme la consommation CPU ou l'empreinte mémoire.
- Gallium HUD, pour mesurer le nombre d'images par seconde affichées.
En combinant ces différents outils dans des scripts Python, nous avons pu dérouler notre séquence de tests de cette manière :
1. Définition de la séquence d'évènements (Cocher une case → Changer d'onglet → Entrer du texte...)
2. Enregistrement des évènements : en même temps que l'application à tester est lancée, le script d'enregistrement demande d'effectuer chaque action définie dans la séquence de test et enregistre ces évènements sous la forme de fichiers yml grâce à libinput record.
3. La séquence d'évènements enregistrée est rejouée grâce à libinput replay. Lorsque cette séquence est rejouée, les métriques (consommation CPU/RAM des processus de l'application, images par seconde affichées...) sont périodiquement mesurées et enregistrées dans un fichier csv. La consommation RAM et CPU vont être récupérées grâce à la bibliothèque psutil qui va lire ces métriques exposées par le noyau (/proc). Quant au nombre d'image par secondes affichées, GALLIUM_HUD, un outil fourni par mesa, qui en ayant accès aux pilotes graphiques, va pouvoir mesurer le nombre d'images par secondes affichées.
Ces données récoltées pourront alors être traitées, visualisées, analysées.
Voici les résultats de la consommation CPU et mémoire moyennes des technologies testées, sur toute la durée du scénario de test. (Ces résultats correspondent à l'étude réalisée sur une Raspberry Pi 3 B+).
- La consommation CPU, prend en compte les 4 cœurs présents sur la carte (100% = Utilisation complète des 4 cœurs)
- La mémoire présente sur la carte est de 1GB.
Ici, l'évolution de la consommation CPU au cours des tests pour les solutions non basées sur des technologies web :
Et ici, l'évolution de la consommation CPU des technologies basées sur des langages web :
En termes d'images par seconde affichées lors de ces tests :
- QML, Flutter, Raygui, QtWebengine et WPE ont atteint les 60 images par seconde lors d'animations.
- LVGL, LVGLJS et Gtk ont atteint ~30 images par seconde. (À noter que la fréquence de rafraîchissement est configurable pour LVGL/LVGLJS. Dans sa configuration de base, la limite est de 30 FPS, ce qui permet une plus faible consommation de ressources et d'énergie)
- Tauri peine à dépasser ~10 images par seconde.
Sur cette cible (Raspberry Pi 3 B+) toutes les technologies testées (sauf Tauri) ont permis une expérience utilisateur acceptable en termes de fluidité, de réactivité. Ces solutions sont donc envisageables pour du matériel au moins équivalent à la Raspberry Pi 3 B+.
Ces tests de performances pourraient d'ores et déjà vous orienter vers un certain choix de technologies. Cependant, je vous invite à continuer à lire cet article pour en apprendre davantage sur ces différentes technologies afin de mieux en appréhender tous leurs avantages et inconvénients.
Je vous présenterai toutes les technologies individuellement à travers :
- L'expérience de développement : en vous donnant des exemples de code, en vous présentant des outils assistant le développement.
- L'intégration sur cible embarquée : lors de cette étude, l'outil utilisé pour l'intégration était Yocto, un projet populaire dans le domaine de l'embarqué. Il facilite la création de distributions Linux embarquées personnalisées en mettant en place une chaîne de compilation adaptée à la cible, en compilant les différents composants logiciels nécessaires et en les assemblant dans une image qui pourra ensuite être utilisée sur la cible.
Qt QML
Je vous propose de commencer par une valeur sûre : Qt. Si vous n'êtes pas encore familier avec Qt, il s'agit d'un framework de création d'interface très populaire.
Qt est à la base d'un bon nombre d'applications, sur toutes sortes de plateformes : bureau, mobile, web, embarqué... Mais aussi, toutes sortes d'OS : Windows, Linux, Android ou même sur bare metal.
Cette bibliothèque s'intègre donc parfaitement dans notre contexte de Linux embarqué.
La partie IHM de Qt se sépare en 2 principaux modules, QtWidgets et QtQuick. QtWidgets est la version historique de Qt, qui se base uniquement sur le C++ pour la création d'interfaces. QtQuick se base sur le langage QML et utilise nativement OpenGL et donc de l'accélération graphique. Cette dernière est donc plus intéressante dans notre cas. Si vous souhaitez en apprendre davantage sur QtQuick et QtWidgets, vous pouvez consulter cet article.
Nous allons donc nous concentrer sur le développement d'applications basées sur QtQuick. Ces applications pourront s'adapter à un grand nombre d'environnements de rendu graphique : EGLFS, Wayland, X11, linuxfb... Une grande flexibilité qui vous permettra d'intégrer vos applications dans toutes sortes d'environnements.
Tous ces avantages ? On attend quoi pour développer ?
Avant de vous lancer dans le développement, il est important de prendre en compte le système de licence de Qt. Qt est basé sur un système de double licence : une licence commerciale et une licence open source : GPLv3/LGPLv3.
Concrètement, en utilisant la licence open-source de Qt, vous serez contraint de mettre à disposition les sources avec votre produit et permettre la mise à jour des composants LGPLv3. Les modalités d'utilisation de cette licence sont contraignantes et peuvent être complexes. Il convient donc d'en discuter avec un professionnel du droit avant la création du produit afin d'être assuré d'en comprendre les implications.
Quant à la licence commerciale, elle vous permettra de faire des liens statiques, de modifier avec moins de contraintes le code et de bénéficier d'un support.
Si vous souhaitez approfondir le sujet, cette présentation vous permettra de mieux appréhender les licences de Qt.
Développement
Pour développer des applications basées sur QtQuick, un langage spécifique à la création d'interface a été conçu : le QML. Il s'agit d'un langage déclaratif visant à simplifier la définition d'interfaces.
Voici quelques exemples simples de composants écrits en QML :
Définition d'un slider
Slider {
id: slider
from: 0
to: 100
value: 25
}
Définition de boutons
Row {
spacing: 20
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Layout.preferredHeight: primaryBtn.height
Layout.topMargin: 12
Button {
id: primaryBtn
text: qsTr("Primary")
anchors.verticalCenter: parent.verticalCenter
onPressed: dialog.open()
}
Button {
id: resetBtn
text: qsTr("Reset")
anchors.verticalCenter: parent.verticalCenter
}
}
Ce langage peut rapidement être pris en mains, la documentation de Qt est très complète avec de nombreux exemples. Il existe également de nombreuses ressources tierces, comme cette série de vidéo réalisée par KDAB, qui facilitent l'apprentissage du QML.
Je développe donc mon interface en QML, mais... Comment interagir avec le système ? Comment lire ou écrire des fichiers, faire des requêtes réseau... 🤔
Le QML, à lui seul, ne sera pas capable d'interagir avec le système : utiliser le système de fichiers, le réseau... Il faudra donc l'étendre avec du code C++. Il existe plusieurs manières de lier le C++ et le QML. Ces différentes méthodes seront à considérer en fonction des besoins des applications. Sur cette page, vous trouverez comment et dans quelles situations utiliser ces techniques.
De nombreux outils existent pour améliorer l'expérience de développement. Voici une liste de quelques outils intéressants :
Qt Creator, un IDE pour le développement d'applications Qt :
QML VSCode, une extension VS Code pour le QML :
Gammaray, un outil de debug :
Yocto
Les applications peuvent facilement être intégrées à des environnements embarqués grâce à son layer meta-qt6. Ce layer fournit des recettes pour chaque module de Qt. Ce qui simplifie grandement la configuration et l'intégration des applications.
Performances
Consommation CPU (%) | Empreinte Mémoire (%) | Maximum Images Par Seconde |
6.28 | 9.56 | 60 |
QtWebengine
Le QML ? Ça me fait peur... On pourrait pas plutôt utiliser des langages Web ?
Qt propose un module, le QtWebEngine, qui permet l'intégration de pages web dans des applications Qt. Ce module va alors intégrer un moteur de rendu web basé sur Chromium. QtWebengine suit les mises à jour de Chromium, il est donc compatible avec les fonctionnalités web les plus récentes.
Les pages web à intégrer pourront donc être développées à l'aide de frameworks web récents comme React, Vue, Angular...
À partir d'ici, vous pourriez vous poser la même question que pour le QML : comment intéragir avec le système ?
Qt fournit également un module, le QtWebchannel qui va permettre cette liaison entre le Javascript de l'interface et le C++ de l'application.
// Main.qml
ApplicationWindow {
visible: true
width: 800
height: 480
title: "QtWebEngine Example"
WebEngineView {
id: webView
anchors.fill: parent
url: "qrc:/QTQuickWeb/index.html"
webChannel: webChannel
}
WebChannel {
id: webChannel
registeredObjects: [jsInterface]
}
JsInterface {
id: jsInterface
WebChannel.id: "JsInterface"
}
Ici, nous allons pouvoir intégrer le Webengine dans la fenêtre de l'application. Dans ce composant Webengine, nous pouvons préciser la page web à afficher dans le champ url. Nous intégrerons également le webchannel, dans lequel nous ajouterons l'objet JsInterface, correspondant aux fonctions C++ à exposer à l'interface.
// main.cpp
class JsInterface : public QObject
{
Q_OBJECT
public:
Q_INVOKABLE float get_cpu_usage() const
{
return stats.get_cpu_usage();
}
};
#include "main.moc"
int main(int argc, char *argv[])
{
QtWebEngineQuick::initialize();
QGuiApplication app(argc, argv);
qmlRegisterType<JsInterface>("JsInterface", 1, 0, "JsInterface");
QQmlApplicationEngine engine;
const QUrl url(u"qrc:/QTQuickWeb/Main.qml"_qs);
QObject::connect(
&engine,
&QQmlApplicationEngine::objectCreationFailed,
&app,
[]() { QCoreApplication::exit(-1); },
Qt::QueuedConnection);
engine.load(url);
return app.exec();
}
Dans le code C++, nous pouvons définir les fonctions à exposer à l'interface sous la forme d'une classe QObject et l'exposer au QML.
// index.html
<html lang="en">
<head>
<title>Embedded UI</title>
<link rel="stylesheet" crossorigin href="qrc:/QTQuickWeb/style.css">
</head>
<body>
<div id="root"></div>
<script src="qrc:/qtwebchannel/qwebchannel.js"></script>
<script src="qrc:/QTQuickWeb/script.js"></script>
</body>
</html>
Voici un exemple de page web intégrée. Ici, nous pouvons intégrer la bibliothèque qwebchannel.js qui va permettre de faire appel aux fonctions C++ exposées.
Si l'application a été développée à partir d'un framework web de type React ou Vue, le résultat de cette page sera contenu dans un fichier js, qui pourra donc être intégré.
À partir de là, il sera possible d'appeler les fonctions C++ exposées à partir de code JavaScript :
// script.js
new QWebChannel(qt.webChannelTransport, function (channel) {
window.jsInterface = channel.objects.JsInterface;
});
...
await window.jsInterface.get_cpu_usage()
L'architecture de l'application se basera alors sur 2 principaux processus :
- Un processus gérant les langages Web et l'affichage de la page (ici, ce processus correspondra à Chromium)
- Un processus s'occupant d'agir sur le système, c'est ici que toute la logique métier se retrouvera.
Ces deux processus communiqueront entre eux grâce au QtWebchannel qui va abstraire une communication via WebSockets
Yocto
QtWebengine a des recettes associées au sein du layer Yocto de Qt. L'intégration de l'application se fera donc simplement en utilisant ces recettes.
Une chose est à noter, si vous souhaitez intégrer une application basée sur QtWebengine, vous devrez avoir en dépendances les recettes : qtwebengine et qtwebengine-tools.
La recette qtwebengine seule n'intégrera pas tous les utilitaires nécessaires à l'application.
Performances
Consommation CPU (%) | Empreinte Mémoire (%) | Maximum Images Par Seconde |
8.39 | 34.89 | 60 |
Gtk
Après Qt, je vous propose maintenant de continuer sur une autre technologie très populaire sur Linux : Gtk. Gtk (GIMP ToolKit) a initialement été créée pour le développement de l'application d'édition d'images GIMP, avant de se détacher de GIMP pour devenir une bibliothèque d'interface à part entière.
Cependant, Gtk est une bibliothèque graphique avec pour principale cible les applications de bureau, basées sur les systèmes de fenêtrages X11/Wayland. Ses exigences en termes de performances graphiques ne sont donc pas aussi élevées que d'autres bibliothèques.
Gtk est sous licence LGPLv2.1, une licence open source plus permissive que celle de Qt. Mais qui reste tout de même assez restrictive en termes de lien et de modification de la bibliothèque originale.
Développement
De base, le développement d'applications Gtk se fait en C. Ici, vous pouvez voir un exemple de code simple d'une application composée d'un bouton :
#include <gtk/gtk.h>
static void on_button_clicked(GtkWidget *widget, gpointer data) {
g_print("Button clicked!\n");
}
int main(int argc, char *argv[]) {
GtkWidget *window;
GtkWidget *button;
gtk_init(&argc, &argv);
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_title(GTK_WINDOW(window), "Hello, GTK!");
gtk_window_set_default_size(GTK_WINDOW(window), 200, 200);
button = gtk_button_new_with_label("Click Me");
g_signal_connect(button, "clicked", G_CALLBACK(on_button_clicked), NULL);
gtk_container_add(GTK_CONTAINER(window), button);
gtk_widget_show_all(window);
g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
gtk_main();
return 0;
}
Gtk propose plus d'une centaine de widgets pour définir tous types d'interfaces et d'applications.
Cependant, malgré l'existence de tous ces composants, certains éléments comme les graphiques peuvent encore venir à manquer. Il existe toutefois des composants proposés par la communauté qui peuvent être utilisables. Mais ces composants peuvent être dispersés, car il n'existe pas d'endroit commun pour les regrouper.
Le développement d'interfaces dans ces conditions peut rapidement devenir complexe. C'est pourquoi différents outils autour de Gtk se sont créés afin d'offrir une meilleure expérience de développement.
Nous pouvons tout d'abord noter qu'il existe des bindings dans différents langages : C++, Javascript, Rust, Python...
Le langage Blueprint se propose également comme alternative pour définir des interfaces Gtk. Ce langage est très similaire au QML.
Le projet Workbench se propose aussi d'améliorer l'expérience de développement en proposant de la prévisualisation en direct, de nombreux exemples, tout en supportant certains bindings.
L'outil Cambalache quant à lui, permet d'éditer l'interface graphiquement.
Yocto
L'intégration Yocto pourra se baser sur des recettes existantes : gtk+3, gtk4 qui sont présentes dans poky.
Une chose à prendre en compte sera l'utilisation d'un compositeur de fenêtre X11/Wayland pour exécuter l'application.
Performances
Consommation CPU (%) | Empreinte Mémoire (%) | Maximum Images Par Seconde |
7.54 | 7.56 | 40 |
Tauri
Pour continuer d'explorer l'écosystème Gtk, je vais vous présenter une technologie d'interface se basant sur la bibliothèque WebkitGTK. Cette bibliothèque porte le moteur de rendu Webkit pour l'environnement Gtk.
Cette bibliothèque a permis la naissance d'un bon nombre de projets : des navigateurs, mais aussi des frameworks de création d'applications.
Tauri est une de ces technologies. Il s'agit d'un projet populaire, très activement développé. Il permet de développer des applications graphiques à partir de langages web et se présente comme un sérieux concurrent à Electron.
Développement
Tauri propose une expérience de développement simple. La CLI proposée simplifie la création, le debug et la construction d'applications Tauri. Pour agir sur le système avec Tauri, une liaison est également mise en place pour appeler des fonctions Rust depuis le code Javascript.
Dans le code, cette liaison se présente de cette façon :
Côté Rust
fn main() {
tauri::Builder::default().invoke_handler(
tauri::generate_handler![get_memory_usage])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
#[tauri::command]
fn get_memory_usage() -> f64 {
let mut sys = System::new();
sys.refresh_memory();
sys.used_memory() as f64 / sys.total_memory() as f64 * 100.0
}
Côté React
import { invoke } from '@tauri-apps/api'
async function get_memory_usage() {
return await invoke('get_memory_usage');
}
Les fonctions à exposer devront être définies avec l'attribut tauri::command et ajoutées à la liste generate_handler.
À partir d'ici, ces fonctions exposées pourront être appelées depuis le JavaScript grâce à la fonction invoke exposée par Tauri.
Similairement à QtWebengine l'architecture de l'application obtenue se basera sur 2 principaux processus traitant d'un côté les langages web et de l'autre agissant sur le système :
Yocto
Tauri ne propose pas de layer ou de recettes Yocto. Un travail d'intégration plus important sera donc à réaliser. Tauri est un projet Rust. Or, il existe un outil nommé cargo bitbake qui permet de générer des recettes Yocto pour des projets Rust. Cet outil est pratique notamment pour la gestion des crates.
Pour intégrer une application basée sur Tauri, il est donc possible de générer des recettes Yocto avec cargo bitbake pour le projet contenant l'application, mais aussi pour le projet contenant la CLI de Tauri.
Ensuite, pour compiler l'application, Tauri utilise une extension de cargo à travers sa CLI. Une possibilité sera donc de créer une classe dérivée de cargo qui permettra l'utilisation des outils de Tauri.
Performances
Consommation CPU (%) | Empreinte Mémoire (%) | Maximum Images Par Seconde |
12.22 | 25.97 | 10 |
WPE
Les tests de performance nous ont montré que les solutions de l'écosystème Gtk ne sont pas les plus adaptées à notre contexte.
Serait-il possible d'utiliser Webkit sans passer par l'environnement Gtk ?
WPE est le projet qui permet cela. Il s'agit du port officiel de Webkit pour les environnements embarqués.
La technologie se concentre sur les performances graphiques, en utilisant au maximum l'accélération matérielle. WPE s'adapte à différents environnements de rendu X11/Wayland/DRM.
Développement
Pour utiliser WPE, la manière la plus simple est d'utiliser le lanceur Cog. Il permet d'afficher une page web en plein écran ou dans une fenêtre en utilisant WPE.
Il est également possible d'intégrer WPE différemment, une documentation de WPE est disponible en ligne.
Pour appeler des fonctions agissant sur le système depuis l'interface, plusieurs méthodes sont utilisables.
Tout d'abord, il est possible d'utiliser un serveur web local pour agir sur le système. Dans ce cas, une communication via Websockets est préférable à une communication via requêtes/réponses HTTP, pour des raisons de performances.
Cependant, il est aussi possible d'aller plus loin dans l'optimisation en utilisant l'API de Webkit. Dans cette vidéo, plusieurs approches sont abordées.
L'architecture choisie pour le test se base sur le lanceur cog et un serveur de websockets local :
Yocto
WPE fournit également un layer Yocto : meta-webkit, qui contient des recettes pour wpewebkit et cog.
Performances
Consommation CPU (%) | Empreinte Mémoire (%) | Maximum Images Par Seconde |
14.06 | 20.67 | 60 |
LVGL
Maintenant, penchons-nous sur une technologie vraiment faite pour l'embarqué.
Ah... Parce que jusqu'à présent, nous n'étions pas sur des technologies embarquées ?
Rassurez-vous, ce que je veux dire par là, c'est que LVGL, par design, a pour objectif les systèmes restreints. Contrairement aux autres technologies qui se sont adaptées à ces contraintes.
LVGL se présente comme :
La plus populaire bibliothèque graphique open source pour créer de belles interfaces pour tout MCU, MPU et écrans.
La bibliothèque est sous licence MIT, une licence très permissive. De plus, les performances matérielles minimales requises sont très basses.
Il s'agit également de la technologie qui s'est illustrée comme la plus performante de notre étude.
Mais, quels sont donc ses défauts ?
Les seuls réels défauts de LVGL sont que la bibliothèque peut être difficile à prendre en mains au début, un certain temps devra être investi avant de pouvoir exploiter pleinement son potentiel. De plus, bien que la communauté de LVGL soit active, elle n'est pas aussi grande que celles d'autres technologies plus populaires. Il peut donc être plus difficile d'obtenir du support sur des problèmes inusuels.
Développement
L'API de LVGL reste tout de même simple à appréhender. Voici un exemple de code simple pour un bouton dans une application LVGL :
lv_obj_t * btn = lv_button_create(lv_screen_active()); /*Add a button the current screen*/
lv_obj_set_pos(btn, 10, 10); /*Set its position*/
lv_obj_set_size(btn, 120, 50); /*Set its size*/
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_ALL, NULL); /*Assign a callback to the button*/
lv_obj_t * label = lv_label_create(btn); /*Add a label to the button*/
lv_label_set_text(label, "Button"); /*Set the labels text*/
lv_obj_center(label);
Des outils tiers existent pour faciliter le développement d'applications LVGL.
Des bindings existent : Micropython, PikaScript, Javascript.
Des outils graphiques ont également vu le jour, notamment Gui Guider qui permet la génération de code LVGL depuis une interface graphique.
Son utilisation est simple et intuitive. Cependant, certaines propriétés, éléments sont manquants. La documentation de LVGL reste bien structurée, ce qui permet aux développeurs de facilement compléter l'application.
Yocto
L'intégration Yocto d'applications LVGL se fait également simplement : LVGL étant une bibliothèque importée depuis ses sources, Yocto pourra tout simplement la recompiler.
En fonction du backend de rendu choisi, des dépendances comme SDL doivent être ajoutées.
Performances
Consommation CPU (%) | Empreinte Mémoire (%) | Maximum Images Par Seconde |
3.19 | 2.65 | 30 |
LVGLJS
Ok, LVGL c'est bien, mais si je ne suis pas à l'aise avec le C ?
Nous avons étudié un des bindings de LVGL : lv_bindings_js. Ce projet permet de définir une interface LVGL à partir d'une syntaxe React. De quoi permettre aux développeurs web aussi d'utiliser cette technologie.
LVGLJS est un projet intéressant pour faciliter l'adoption de LVGL, mais le projet fait encore face à différents problèmes comme une documentation incomplète, un coût important en consommation CPU.
De plus, le projet souffre d'un manque de contributeurs et de mainteneurs, ce qui rend les évolutions assez lentes.
Développement
L'expérience de développement avec LVGLJS est très similaire à une expérience de développement d'application web classique.
On va retrouver une syntaxe semblable au HTML avec des balises. À la seule différence que ces balises correspondront à des composants LVGL :
<View></View>
<Checkbox></Checkbox>
<Button></Button>
...
La liste des composants supportés est disponible dans la documentation.
Au niveau du style, nous retrouverons des propriétés équivalentes au CSS, qui devront être ajoutées directement aux composants :
<View style={style.radioGroup}>
<Checkbox text='Radio 1'/>
<Checkbox text='Radio 2'/>
<Checkbox text='Radio 3'/>
</View>
const style = {
radioGroup = {
'display': 'flex',
'flex-direction': 'column',
'width': '100%',
'align-items': 'center',
'height': 'auto',
}
}
Enfin, pour agir sur le système, LVGLJS propose une API JavaScript permettant de lire, d'écrire des fichiers, de faire des requêtes HTTP...
L'API reste cependant très limitée, la solution préférée lors de notre test a été la mise en place d'un serveur web local agissant sur le système à la réception d'une requête HTTP :
async function getCurrentCpuUsage() {
const response = await fetch('http://localhost:8000/cpu_usage')
const data = parseFloat(await response.text())
return data
}
L'architecture de l'application obtenue se base donc sur plusieurs processus :
Cependant, une meilleure solution qui permettrait d'éviter cette architecture multi-processus serait d'étendre l'API JavaScript de LVGLJS.
Yocto
Les scripts de build de LVGLJS ne permettent pas une intégration Yocto directe. Ils ont dû être adaptés pour permettre l'utilisation de la toolchain Yocto.
De plus, LVGLJS intègre ses dépendances en tant que sous-modules git. Or, certaines de ces dépendances ont des recettes Yocto associées qui peuvent être utilisées.
De plus, la version du sous-module lv_drivers intégré n'est pas sur une version stable, ce qui peut également causer des problèmes.
Performances
Consommation CPU (%) | Empreinte Mémoire (%) | Maximum Images Par Seconde |
25.15 | 4.73 | 30 |
Raygui
Raygui, similairement à LVGL est une bibliothèque d'interface assez minimale, développée en C.
La particularité de cette bibliothèque est qu'elle se base sur une stratégie de rendu dite Immediate Mode. Cette stratégie va parcourir le code en continu afin de détecter et réagir à des évènements utilisateurs.
Cette particularité implique donc que la consommation CPU de l'application ne tombera jamais à zéro, mais restera stable pendant toute la durée de vie de l'application.
Raygui a cependant un design de composants de base particulier, assez loin des standards, qui peut être un frein pour son adoption.
De plus, l'interface se développe directement en C, pas de langage dédié à la création d'interface n'est disponible.
Développement
Exemple de code raygui
void GuiTab1(GuiTab1State *state)
{
// Draw controls
if (state->DropdownBox010EditMode) GuiLock();
GuiPanel(Rectangle{ state->anchor01.x + 0, state->anchor01.y + 32, 800, 400 }, NULL);
GuiCheckBox(Rectangle{ state->anchor02.x + 24, state->anchor02.y + 32, 24, 24 }, "Check Box 1", &state->CheckBoxEx001Checked);
GuiCheckBox(Rectangle{ state->anchor02.x + 24, state->anchor02.y + 64, 24, 24 }, "Check Box 2", &state->CheckBoxEx002Checked);
GuiCheckBox(Rectangle{ state->anchor02.x + 24, state->anchor02.y + 96, 24, 24 }, "Check Box 2", &state->CheckBoxEx003Checked);
GuiGroupBox(Rectangle{ state->anchor02.x + 0, state->anchor02.y + 8, 144, 136 }, "Checkboxes");
GuiGroupBox(Rectangle{ state->anchor03.x + 0, state->anchor03.y + 0, 144, 144 }, "Radios");
GuiLabel(Rectangle{ state->anchor01.x + 312, state->anchor01.y + 200, 168, 16 }, "This is a descriptive label");
GuiListView(Rectangle{ state->anchor01.x + 600, state->anchor01.y + 96, 144, 312 }, join(state->ListViewValues, ";").c_str(), &state->ListView012ScrollIndex, &state->ListView012Active);
GuiSlider(Rectangle{ state->anchor01.x + 312, state->anchor01.y + 288, 168, 16 }, NULL, NULL, &state->Slider016Value, 0, 100);
GuiToggleGroup(Rectangle{ state->anchor03.x + 24, state->anchor03.y + 24, 96, 24 }, "Radio 1\nRadio 2\nRadio 3", &state->RadioGroupActive);
if (GuiDropdownBox(Rectangle{ state->anchor01.x + 312, state->anchor01.y + 216, 168, 24 }, "First;C++;Linux;Archlinux", &state->DropdownBox010Active, state->DropdownBox010EditMode)) state->DropdownBox010EditMode = !state->DropdownBox010EditMode;
GuiUnlock();
}
Il existe toutefois des outils graphiques pour assister la création d'interfaces comme :
rGuiLayout pour gérer le placement de composants.
rGuiStyler pour gérer la personnalisation de composants.
Ces outils ne permettront pas de développer entièrement une application, des adaptations, des interactions devront être implémentées. Certains composants classiquement utilisés sont manquants, comme le clavier virtuel ou le graphique, qui doivent donc être développés.
Yocto
Cette technologie est assez minimale, il s'agit d'une bibliothèque C sans réelle dépendance, l'intégration sera donc assez simple.
Cependant, Raygui est basée sur Raylib. Une solution possible pour réaliser l'intégration Yocto est de créer une recette pour Raylib, pour ensuite la lier à Raygui et à l'application.
Performances
Consommation CPU (%) | Empreinte Mémoire (%) | Maximum Images Par Seconde |
6.18 | 2.17 | 60 |
Flutter
Enfin, pour terminer sur une technologie plus populaire, intéressons nous à Flutter.
Flutter est une technologie open source développée par Google qui ne cesse de gagner en popularité.
Le réel avantage de la technologie est qu'elle permet de développer des applications cross-platform, desktop, mobile, web depuis une seule code base.
La technologie a atteint une maturité qui lui a permis d'être adoptée par un bon nombre d'entreprises.
Desktop, mobile, web... Et l'embarqué dans tout ça ?
Des projets embarqués basés sur Flutter ont également vu le jour, avec notamment Toyota qui utilise cette technologie pour leurs nouveaux systèmes d'infodivertissement.
Cette technologie met l'accent sur l'expérience utilisateur, avec notamment un support de l'écran tactile avancé, une fluidité accrue, un design soigné...
Développement
Le langage utilisé par Flutter est le Dart. Ce langage a été conçu pour être utilisé avec Flutter et donc créer des interfaces. Il est donc simple et rapide de définir des interfaces Flutter.
Dart n'est pas limité à la définition d'interfaces, il peut être utilisé dans tous types de contextes. Des bibliothèques officielles permettent d'abstraire des fonctionnalités d’interaction avec l'OS.
Cependant, il est aussi possible de lier l'interface à un autre langage comme le C grâce au paquet FFI (Foreign Function Interfaces) de Dart.
Voici un exemple de code Flutter définissant un simple slider :
class _SliderWidgetState extends State<SliderWidget> {
late double _value;
@override
void initState() {
super.initState();
_value = widget.initialValue;
}
@override
Widget build(BuildContext context) {
return Slider(
value: _value,
min: 0.0,
max: 100.0,
divisions: 100,
label: '$_value',
onChanged: (value) {
setState(() {
_value = value;
});
widget.onChanged(value);
},
);
}
}
Flutter soigne l'expérience de développement. La CLI de Flutter propose un bon nombre d'outils utiles au développement. D'autres outils sont également fournis par Flutter pour faciliter la création et le debug d'applications.
Une extension VS Code officielle est par ailleurs fournie, proposant de l'autocomplétion et intégrant ces différents outils de debug.
Les applications Flutter se basent sur 3 principales couches :
- Flutter Framework : il s'agit du composant le plus haut niveau des applications. Il s'occupera des premières phases de rendu de l'interface.
- Flutter Engine: ce composant va s'occuper de la dernière étape de rendu : la rastérisation, qui consiste à convertir la représentation des images obtenues en matrices.
- Flutter Embedder: ce composant s'occupera quant à lui de gérer toutes les interactions avec l'OS et le matériel. Différents embedders ont vu le jour afin d'optimiser les performances des applications dans des contextes embarqués.
Pour plus de détails sur l'architecture de Flutter, une page de la documentation y est dédiée.
Yocto
Les applications Flutter peuvent être intégrées à Yocto grâce au layer meta-flutter. Ce layer va fournir des recettes pour intégrer le moteur Flutter, utilisé par les applications.
Il va également proposer des recettes pour certains embedders qui sont des parties de code visant à optimiser l'interaction entre le moteur Flutter et un certain matériel.
Les applications Flutter développées pourront facilement être intégrées grâce à la classe flutter-app.
Ces applications pourront ensuite être exécutées grâce aux embedders, qui servent de lanceurs.
Performances
Consommation CPU (%) | Empreinte Mémoire (%) | Maximum Images Par Seconde |
7.57 | 5.92 | 60 |
Conclusion
Nous avons pu voir que les technologies d'interfaces sont nombreuses, et cet article n'en a exploré qu'une poignée. Cependant, les solutions qui se concentrent spécifiquement sur le domaine de l'embarqué restent assez limitées.
Cette étude permet toutefois de tirer quelques conclusions qui pourraient vous aider à réaliser des choix de technologies.
Si votre matériel est très limité, le choix de se tourner vers une technologie comme LVGL semble judicieux, car cette bibliothèque s'adapte bien à ce genre d'environnements.
Si vous disposez de matériel plus performant et que vous souhaitez mettre l'accent sur l'expérience utilisateur, vous pourriez plutôt vous tourner vers Qt ou Flutter.
Enfin, si vous voulez vous tourner vers des solutions basées sur des technologies web, WPE et QtWebengine paraissent être les meilleures alternatives.
Nous pouvons également noter que tous ces projets sont encore en pleine évolution. De nouveaux outils, de nouvelles mises à jour pourraient contribuer à améliorer l'expérience de développement et les performances des applications.
Nous pouvons notamment noter le lancement d'un projet d'éditeur d'interface, qui permettra l'utilisation d'un langage spécifique pour LVGL, et, à termes, de pouvoir convertir des designs développés à partir d'outils comme Figma.
Une alternative à suivre dans le domaine des moteurs de navigateurs est le projet Servo. Ce moteur de navigateur pourrait être intéressant à l'avenir, car des efforts sont faits pour qu'il soit performant sur des environnements embarqués. De plus, il pourrait être intégré à Tauri, ce qui serait encourageant pour les technologies web dans l'embarqué.