Depuis que le système de domotique est doté d’un processeur, la carte choisie a été l’Arduino Méga 2560 Rev3 (en réalité un clone de marque Elegoo). Cet Arduino dispose de nombreux ports (digitaux, analogiques et pour les protocoles tels que plusieurs UART, du SPI…).
J’ai hésité lors d’une mise à jour majeure du système à changer de carte pour passer sur un ESP, bien plus puissant et riche en fonctionnalités. Cependant, j’ai décidé de rester en terrain connu et de faire avec les moyens de l’Arduino (en particulier, j’appréciais le confort du 5 V pour tout contrôler). Aujourd’hui, j’aurai peut-être fait un autre choix, mais il n’en reste que l’Arduino fait correctement son travail.
La partie circuiterie est traitée dans un autre article. Ici, nous nous concentrerons sur l’architecture du programme de l’Arduino Méga (il y a aussi un article sur la partie connexion à Home Assistant et l’ESP8266-01).
L’intégralité du programme est disponible en source ouverte sur mon dépôt GitHub.
Les principes de base
Les premiers programmes étaient un amas de ligne de code désorganisé et peu optimisé. Le tout était regroupé dans un seul et même fichier, pour dire le bordel que c’était…
Dans la version actuelle, je suis reparti de zéro, en introduisant plusieurs nouveautés :
- L’abandon de l’EDI Arduino pour passer à PlatformIO sur Visual Studio Code. Cela me permet de mieux gérer le multifichier, le dépôt git, et de nombreuses autres fonctionnalités (analyse du programme, suggestions de complétion…).
- L’utilisation de la programmation orientée objet. C’était une première pour moi ! Le but étant d’avoir un programme plus propre et modulaire. Je peux facilement intégrer un nouvel appareil si ses fonctionnalités sont déjà implémentées. Les touches de menu seront automatiquement ajoutées, et il ne suffira que d’instancier l’appareil et de l’ajouter dans quelques listes du
main.cpp
. - La création d’un code propre et respectant les conventions (je pense que mon code est loin d’être parfait, mais j’ai essayé d’appliquer le plus de principes que j’ai lu dans diverses documentations).
- L’optimisation : réduction des blocs de code bloquant (comme les animations sur l’écran).
La structure globale du programme
Fichier principal
Le fichier main.cpp n’est pas très long. Il a les rôles suivants :
- Déclarer les bibliothèques et les autres fichiers du programme utilisés.
- Instancier tous les périphériques des classes définies dans le répertoire
device/
. - Ordonner tous les objets instanciés dans diverses listes.
- Appeler chaque objet de la liste des périphériques pour les initialiser.
- Et enfin, appeler chaque fonction de
loop()
disponible.
La plus grosse partie du code régissant toutes les fonctionnalités se trouve dans les classes des périphériques. Il est très facile d’ajouter un appareil dont les spécificités sont déjà implémentées en ajoutant quelques lignes à ce main.cpp
.
Répertoire des périphériques
Dans le répertoire device/
, se trouvent toutes les classes des composantes du système : capteur, actionneurs et interfaces.
Les classes ont de nombreuses relations entre-elles, avec notamment de l’héritage. La classe commune à tous les composants est la classe Device
. Celle-ci est très simple : elle définit le nom, l’identifiant unique, et assure les fonctions de base comme le setup()
, le loop()
et la récupération des attributs. Plusieurs classes héritent de Device
:
- La classe
Input
, qui intègre en plus la connexion à Home Assistant (voir l’article dédié). - La classe
Output
, qui intègre les méthodes de gestion d’un appareil « basique » : l’allumer, l’éteindre, récupérer son état… Il est possible de bloquer un appareil si sa gestion doit être réservée à une seule fonctionnalité du système (par exemple pour les musiques animées). En plus de cela, la gestion de Home Assistant est déclarée. - Les périphériques du répertoire
interface/
héritent directement de la classeDevice
, car ceux-ci ne sont pas intégrés à Home Assistant et leur fonctionnement est particulier.
Par la suite, les classes fille des classes décrites ci-dessus implémentent les fonctionnalités spécifiques à un type de périphérique.
Le répertoire utils/
contient diverses fonctions utiles au système : correction gamma, variables globales (il y en a peu !), et un système d’économie de mémoire vive (stockage de chaînes de caractère dans la mémoire flash).
Enfin, les autres fichiers du répertoire racine (src/
) sont dédiés à la définition de valeurs pour le programme :
- Broches de l’Arduino.
- Identifiants uniques des périphériques (utilisé pour la communication avec ESPHome).
- Utilisation de la mémoire EEPROM (définition des adresses utilisées pour chaque fonction).
- Pictogrammes utilisés par l’écran.
- Les musiques du système de musique animées.
Quelques points intéressants
Le clavier
Le clavier du boîtier de contrôle intègre un système complet de menus. Ceux-ci sont générés automatiquement.
Lors de l’initialisation du programme, un Keypad
est instancié et plusieurs listes sont passées en paramètre, de tous les types de périphériques disponibles (lumières, capteurs, télévisions…). Ces listes sont utilisées pour générer toute une arborescence de menus : les menus principaux (lumières, périphériques, alarmes, télévisions, capteurs et configuration) et les sous-menus (contrôle poussé d’une lumière…). Ce système est très modulaire puisque l’ajout d’un périphérique ne nécessite aucune modification de la classe Keypad
: il suffit de l’ajouter dans la liste. Par la même occasion, les aides sont générées en récupérant les noms des périphériques.
Chaque menu est une instanciation d’une classe. Il n’y a pas de liste complète des menus existants dans le programme puisque la navigation se fait relativement. Je m’explique : la classe KeypadMenu
(représentant un menu) enregistre le menu suivant (lorsqu’on presse la touche D), le menu précédent (lorsqu’on presse la touche C), le menu parent (quand on presse la touche B), et le menu principal (lorsqu’on presse la touche A). Certaines touches du menu peuvent être associées à un autre menu. Par exemple, à l’appui long sur la touche de la lampe de chevet dans le menu des lumières, le menu de contrôle de la lampe de chevet est ouvert.
L’écran
L’écran est un OLED de 128×64 pixels. Il est utilisé pour accompagner la navigation au clavier, avec l’affichage du menu actuel, des actions effectuées, et de l’aide. La majorité des appareils du système de domotique ont l’écran d’enregistré parmi leurs attributs. Cela leur permet d’afficher des animations (par exemple lors de l’allumage d’une lumière). La classe de l’écran propose simplement plusieurs types d’affichage et d’animation.
La bibliothèque fournie ne prend pas en charge nativement les accents dans les chaînes de caractère. Pour en afficher, il faut appeler une fonction spécifique avec le code ASCII du caractère voulu… Ce qui n’est pas pratique. J’ai donc mis en place un système permettant de contourner ce problème : une fonction prend en paramètre une chaîne de caractère et l’analyse pour manuellement afficher les caractères spéciaux (accents). Il faut aussi indiquer le placement du début du mot puisque la fonction écrit elle-même le texte sur l’écran. Une autre fonction affiche un texte centré sur l’écran. Là encore, les accents sont embêtants ! En comptant la longueur de la chaîne de caractère, les lettres accentuées comptent pour deux… J’ai donc contourné ce problème avec ma fonction.
Les optimisations de mémoire vive
Cette version du programme aura aussi été une guerre contre le remplissage de la mémoire vive. J’ai finalement gagné, mais le combat aura été long !
Le programme prenant du volume, j’en suis arrivé à un point ou le programme plantait par manque d’espace dans la mémoire vive. La faute aux chaînes de caractère String
que je déclarais dans tous les sens… Cette classe est très pratique, mais très gourmande ! J’ai donc dû innover et trouver des solutions pour contourner le problème. La solution que j’ai trouvée consiste à stocker les chaînes de caractère dans la mémoire flash par plusieurs moyens. C’est pour cela que les friendlyName
des périphériques sont déclarés avec un F()
autour. Je ne suis pas très à l’aise avec tous ces mécanismes d’optimisation, donc je vous laisse vous documenter vous-même et voir comment cela fonctionne dans le programme !
L’Arduino Méga 2560 dispose d’une grande mémoire flash, si bien que je suis tranquille pour longtemps. À la dernière compilation, le programme occupait 48 % de la mémoire flash.
La télévision
J’ai décrit plus en détail le fonctionnement de la télévision dans l’article sur le fonctionnement des périphériques. Je ne vais donc pas me répéter. Néanmoins, je vais expliquer le système de musiques animées. Le concept est simple : une musique avec son clip est diffusée sur la télévision, et les périphériques s’allument et s’éteignent en synchronisation avec la musique.
Le lancement s’opère depuis le clavier de contrôle. Le programme demande à Home Assistant de jouer la vidéo demandée puis se met en attente du chargement. La vidéo commence par un bip de trois fréquences : 1000 Hz, 1500 Hz et 2000 Hz. Ceux-ci sont détectés par le microphone (je n’ai pas réussi à effectuer une transformation de Fourrier avec l’Arduino qui est peu puissant ; j’ai donc un système moins précis, mais qui fonctionne). Avoir plusieurs fréquences augmente la chance que la détection fonctionne tout en gardant de la précision puisqu’à la détection d’une fréquence, l’Arduino détermine précisément l’avancement de la vidéo. Par la suite, la musique commence et le programme lit une suite d’action que j’ai écrite : allumage et arrêt de périphériques, contrôle des couleurs…
La création de musique animée est un long processus, car chaque action doit être définie manuellement. J’ai tenté d’automatiser le processus en utilisant des fichiers MIDI, mais le résultat ne fut pas concluant. La liste des actions se présente sous la forme suivante : un temps en milliseconde est associé à une action qui suit le protocole que j’ai défini.
C’est pas terminé !
Je ne vous ai présenté qu’un des deux programmes composant le système ! Dans le prochain article, nous allons voir comment connecter tout cela.
Laisser un commentaire