Cette semaine, nous nous intéressons au framework Electron. Nous allons voir comment réaliser une application multiplateforme qui utilise nos modules.
Electron est un framework JavaScript qui permet de développer des applications multiplateformes. Mais comment fonctionne-t-il?
En fait, Electron est l'assemblage de plusieurs technologies: HTML, CSS et JavaScript. En fait, il s'agit d'un browser Web Chromium customisé pour utiliser Node.js et afficher une page HTML. De cette manière, le code JavaScript d'une application Electron peut utiliser presque toutes les fonctionnalités de l'OS. Il est par exemple possible d’accéder au filesystem de la machine, ou d'exécuter n'importe quelle commande directement depuis la page Web. Au final, cela permet de réaliser une application qui a l'air native, mais qui est une page Web.
Un autre avantage de cette solution est qu'elle permet de packager une application sur n'importe quelle plateforme supporté par Chromium, ce qui veut dire Windows, Linux et macOS.
Maintenant que nous avons dégrossi le fonctionnement d'Electron, regardons comment utiliser nos modules dans une telle application.
Note: Afin de garder cet article relativement digeste, nous allons partir du principe que vous arrivez à écrire une application basique avec Electron. Si vous n'avez jamais utilisé ce framework, nous vous conseillons de commencer par la section "Getting Started" de la documentation d'Electron et de revenir après.
De la même manière, si vous n'avez jamais utilisé notre librairie JavaScript / EcmaScript 2017, il serait judicieux de commencer par cet article .
Ajouter la librairie Yoctopuce
Nous allons partir de l'application electron-quick-start de la documentation Electron et modifier le code pour afficher la liste des modules connectés par USB.
Tout d'abord, il faut ajouter une dépendance à notre librairie JavaScript / EcmaScript 2017 dans le fichier package.json et l'installer à l'aide de la commande "npm install".
Le fichier package.json avec la dépendance sur la librairie yoctolib-es2017.
"name": "demo_electron",
"ProductName": "Electron demo app",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "electron .",
},
"dependencies": {
"yoctolib-es2017": "^1.10.38168"
},
"devDependencies": {
"electron": "^7.1.3",
}
}
Il faut ensuite modifier la page HTML de l'application, pour ajouter un tag <ul> que nous remplirons avec la liste des modules détectés.
<body>
<h2 class="section_title">Module inventory:</h2>
<div class="section">
<ul id="module_list"></ul>
</div>
...
Ensuite, il ne "reste plus qu'à" écrire le code JavaScript qui va remplir cette liste.
let serial_list = [];
function refresh_module_list()
{
let ul = document.getElementById('module_list');
ul.innerHTML = '';
for (let i = 0; i < serial_list.length; i++) {
let li = document.createElement("li");
li.appendChild(document.createTextNode(serial_list[i]));
ul.appendChild(li);
}
}
async function deviceArrival(module)
{
let serial = await module.get_serialNumber();
serial_list[serial_list.length] = serial;
refresh_module_list();
}
async function deviceRemoval(module)
{
let serial = await module.get_serialNumber();
serial_list = serial_list.filter(item => item !== serial);
refresh_module_list();
}
function handleHotPlug()
{
YAPI.SetTimeout(handleHotPlug, 1000);
}
async function startDemo()
{
await YAPI.LogUnhandledPromiseRejections();
try {
// Setup the API to use the VirtualHub on local machine
await YAPI.RegisterHub('localhost');
} catch () {
ipcRenderer.send('open-error-dialog', errmsg.msg);
return;
}
await YAPI.RegisterDeviceArrivalCallback(deviceArrival);
await YAPI.RegisterDeviceRemovalCallback(deviceRemoval);
handleHotPlug()
}
startDemo();
Le code est simple, on initialise la librairie avec la méthode YAPI.RegisterHub et on enregistre deux callbacks pour détecter les plug et unplug des modules. La seule particularité de ce code par rapport à l'utilisation de notre librairie dans un browser Web est l'utilisation de la fonction require pour charger les fichiers de notre librairie.
Notre application Electron
Utilisation du VirtualHub
Même avec Electron et Node.js, il est impossible d'accéder directement aux périphériques USB depuis du code JavaScript. Par conséquent, pour utiliser les modules branchés sur les ports USB, il faut que le VirtualHub soit exécuté. Si le VirtualHub n'est pas lancé lors de l’exécution de YAPI.RegisterHub('localhost'), cette méthode retournera une erreur "connect ECONNREFUSED 127.0.0.1:4444".
Heureusement, il est possible d'inclure le VirtualHub dans notre application Electron et de le lancer automatiquement au démarrage de l'application. De cette manière, il est possible de "cacher" le VirtualHub et que l'utilisateur n'ait qu'un seul exécutable à lancer.
Pour ajouter le VirtualHub à l'application Electron, il suffit de le copier dans le répertoire comme n'importe quelle autre ressource. Dans cet exemple, nous avons choisi de copier toutes les versions du VirtualHub dans les sous-répertoires suivants:
- Windows : VirtualHub/windows/VirtualHub.exe
- Linux Intel 64 bits : VirtualHub/linux/64bits/VirtualHub
- Linux Intel 32 bits : VirtualHub/linux/32bits/VirtualHub
- Linux ARM (Raspberry Pi): VirtualHub/linux/armhf/VirtualHub
- Linux ARM 64 bits: VirtualHub/linux/aarch64/VirtualHub
- macOS : VirtualHub/osx/VirtualHub
Il faut ensuite écrire une fonction qui détecte l'OS et l'architecture de la machine et lance l’exécutable correspondant.
{
let arch = os.arch();
let ostype = os.type().toLowerCase();
let executablePath = "";
if (ostype.startsWith("win")) {
executablePath = "windows/VirtualHub.exe";
} else if (ostype === 'linux') {
if (arch === 'x64') {
executablePath = "linux/64bits/VirtualHub";
} else if (arch === 'ia32') {
executablePath = "linux/32bits/VirtualHub";
} else if (arch === 'arm64') {
executablePath = "linux/aarch64/VirtualHub";
} else if (arch === 'arm') {
executablePath = "linux/armhf/VirtualHub";
}
} else if (ostype === 'darwin') {
executablePath = "osx/VirtualHub";
}
let full_path = "./VirtualHub/" + executablePath;
vhub_process = execFile(full_path, ['-y'], {cwd: app.getAppPath()},
function (error, stdout, stderr) {
if (vhub_ignore_error) {
return;
}
if (stderr) {
console.log("ERR:" + stderr.toString());
dialog.showErrorBox('VirtualHub error', stderr.toString());
} else {
console.log(stdout.toString());
}
if (error) {
console.error(error);
dialog.showErrorBox('VirtualHub error', error.toString());
return;
}
});
}
Dans le fichier main.js, on modifie le code pour appeler cette fonction avant la création de la fenêtre.
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', function(){
startVirtualHub();
createWindow();
});
Il est maintenant possible de tester notre application Electron à l'aide de la commande:
npm start
Notre application Electron sous macOS
Particularité de Linux
Pour fonctionner correctement, le VirtualHub a besoin d'avoir accès en écriture aux périphériques USB Yoctopuce. Or, par défaut, la plupart des distributions Linux bloque les accès en écriture pour les utilisateurs non-root. Une solution est d'installer une règle udev sur la machine pour changer ce comportement, comme expliqué dans cet article.
L'autre solution est d'utiliser la commande sudo pour octroyer les droits root à l'application. Mais, dans ce cas, c'est Electron qui n'est pas content:
yocto@linux-laptop:~/electron/electron_example$ sudo npm start > demo_electron@1.0.0 start /home/yocto/electron/electron_example > electron . [5963:1212/000704.995338:FATAL:atom_main_delegate.cc(211)] Running as root without --no-sandbox is not supported. See https://crbug.com/638180.
Comme le dit le message d'erreur, il faut ajouter l'option --no-sandbox au démarrage de l'application. Cependant, comme notre application n'est pas encore packagée et que nous utilisons npm pour appeler Electron, cette option est interprétée par npm et non par Electron.
L'astuce est d'ajouter une nouvelle commande "start_as_root" qui instancie Electron avec l'option --no-sandbox dans le fichier package.json.
"name": "demo_electron",
"ProductName": "Electron demo app",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "electron .",
"start_as_root": "electron . --no-sandbox",
},
"dependencies": {
"yoctolib-es2017": "^1.10.38168"
},
"devDependencies": {
"electron": "^7.1.3",
}
}
De cette manière, il est possible pendant le développement de lancer l'application en tant que root à l'aide de la commande:
sudo npm run start_as_root
Packager l'application
En l'état, l'application est fonctionnelle mais nécessite d’avoir installé Node.js et npm. C'est fort pratique pour tester l'application lors du développement mais pour que l'application soit facilement distribuable il faut la packager à l'aide du package electron-packager.
Ce package permet de générer un application complètement autonome et distribuable sur n'importe quelle machine.
Si ce n'est pas déjà fait, il faut ajouter electron-packager à la liste des packages de développement à l'aide de la commande suivante:
npm install electron-packager --save-dev
Ensuite, on ajoute au fichier package.json une commande qui exécute electron-packager. Cette commande prend en argument l'OS, l’architecture ainsi que l'icône à utiliser pour l’exécutable.
Voici notre fichier package.json final:
"name": "demo_electron",
"ProductName": "Electron demo app",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "electron .",
"start_as_root": "electron . --no-sandbox",
"package-mac": "electron-packager . --overwrite --platform=darwin --arch=x64 --icon=icons/logo_black_1024.png ",
"package-win": "electron-packager . --overwrite --platform=win32 --arch=ia32 --icon=icons/logo_black.ico --out=release-builds ",
"package-lin-x64": "electron-packager . --overwrite --platform=linux --arch=x64 --icon=icons/logo_black_1024.png ",
"package-lin-ia32": "electron-packager . --overwrite --platform=linux --arch=ia32 --icon=icons/logo_black_1024.png ",
"package-lin-arm": "electron-packager . --overwrite --platform=linux --arch=armv7l --icon=icons/logo_black_1024.png ",
"package-lin-arm64": "electron-packager . --overwrite --platform=linux --arch=arm64 --icon=icons/logo_black_1024.png "
},
"dependencies": {
"yoctolib-es2017": "^1.10.38168"
},
"devDependencies": {
"electron": "^7.1.3",
"electron-packager": "^14.1.1"
}
}
Nous avons 6 commandes package-mac, package-win, package-lin-x64, package-lin-ia32, package-lin-arm et package-lin-arm64 qui permettent respectivement de packager/compiler l'application pour macOS, Windows, Linux Intel 64 bits, Linux Intel 32 bits, Linux ARM, Linux ARM 64 bits.
Chaque commande crée un sous-répertoire, avec tous les fichiers nécessaires à faire fonctionner l'application et un exécutable "demo_electron" qui peut être utilisé comme n'importe quelle application native.
Par exemple, pour générer l'application pour un Raspberry Pi depuis Windows, il suffit de lancer la commande suivante:
npm package-lin-arm
Cette commande va créer un sous-répertoire demo_electron-linux-armv7l
avec tout ce qui est requis pour faire fonctionner l'application.
Il suffit de copier tout ce répertoire sur le Raspberry Pi et de lancer l’exécutable demo_electron pour démarrer l'application.
L'application pour Raspberry Pi compilée depuis Windows
Les sources de notre exemple
Les source de cette application sont disponibles sur GitHub: https://github.com/yoctopuce-examples/electron_example
En plus du code que nous venons d'expliquer, elle permet de contrôler les LEDs d'un Yocto-Color-V2 et d'afficher les mesures d'un Yocto-Meteo ou Yocto-Meteo-V2.
L'application est basique, mais elle fonctionne sur les plateformes suivantes:
- Windows
- macOS
- Linux (Intel et ARM)
Conclusion
Ce framework est très intéressant car il permet de réaliser facilement de petites applications qui fonctionnent sur n'importe quelle plateforme. De plus, il existe des packages qui permettent de générer des installeurs pour Windows ou des fichiers .deb pour Linux.