Écran OLED et interface utilisateur (HMI) ========================================= .. note:: **Document d'architecture, partiellement implémenté.** Le pilote SSD1309 et l'affichage d'une image existent depuis le 2026-09-07 : le logo s'affiche au démarrage, avant micro-ROS. Le reste — expressions, animations, menus, topics ``screen/`` — reste à écrire, par phases (voir la fin de page). Le format des images, et pourquoi il faut transposer ---------------------------------------------------- Les images sont produites par **lcd-image-converter**. Les réglages du preset ``Monochrome`` doivent rester ceux-ci : .. list-table:: :header-rows: 1 :widths: 34 16 50 * - Réglage - Valeur - Pourquoi * - ``bits per pixel`` - 1 - écran monochrome * - ``data block size`` - 8 bit - un octet par groupe de 8 pixels * - ``split to rows`` - **yes** - donne 16 octets par ligne : la conversion en dépend * - ``main scan direction`` - top_to_bottom - ordre des lignes * - ``line scan direction`` - forward - bit 7 = pixel de gauche * - ``RLE compression`` - no - on ne décompresse pas côté carte * - ``inverse`` - no - sinon on obtient le négatif Le type de conversion (``Diffuse Dither``, seuil…) est **libre** : c'est un choix esthétique, le tramage rend mieux les dégradés, le seuil est plus net sur du trait. .. important:: **Les deux dispositions font 1024 octets et n'ont rien à voir.** .. code-block:: text convertisseur : 16 octets par LIGNE, bit 7 a gauche, 64 lignes SSD1309 : 8 PAGES de 128 colonnes, un octet = 8 pixels VERTICAUX Envoyer l'une pour l'autre ne donne pas une image décalée : ça donne du bruit. ``images_rows_to_pages()`` transpose au chargement, une fois, pour que toute image régénérée avec les mêmes réglages fonctionne telle quelle. Ajouter une image ~~~~~~~~~~~~~~~~~ Une commande, rien d'autre : .. code-block:: bash ./scripts/ratbot-image conception/img/Image_fache.c Le script dérive le symbole du nom de fichier, applique les retouches mécaniques, dépose le fichier dans ``App/images/`` et **régénère la galerie**. .. important:: Pourquoi un script plutôt qu'un réglage dans l'outil : lcd-image-converter nomme **tous** ses documents ``Image`` par défaut, et c'est ce nom qui devient l'identifiant C. Six exports, six fois ``image_data_Image`` — collision à l'édition de liens. Le nom se change dans l'outil (c'est l'attribut ``name`` du document, visible dans le ``.xml``), mais il faut y penser à **chaque** image. Le script le fait une fois pour toutes. La galerie ~~~~~~~~~~ ``images_gallery.[ch]`` sont **générés** — ne pas les éditer. Ils exposent une table nom → image : .. code-block:: c const tImage *img = images_find("fache"); C'est ce qui permettra à ``screen/command`` de désigner une expression par son nom depuis ROS, sans que le Pi connaisse la disposition mémoire de l'écran. Piloter l'écran depuis ROS -------------------------- .. code-block:: bash ros2 topic pub --once /ratbot_1/screen/expression std_msgs/msg/String "data: fache" ros2 topic echo /ratbot_1/screen/button Un nom absent de la galerie **ne change rien** — effacer l'écran sur une faute de frappe serait pire que d'ignorer la demande. Le compteur ``noms_inconnus`` de ``/diagnostics`` le signale, et le bloc passe en WARN. Les boutons sont publiés sur le **front montant** seulement : publier aussi le relâchement doublerait le trafic pour une information dont un menu n'a que faire. .. note:: Le contrat prévoit ``screen/command`` en ``ratbot_msgs/ScreenCommand``, un message **personnalisé** — il impose de régénérer la bibliothèque micro-ROS avec un paquet de plus. ``screen/expression`` (``std_msgs/String``) est la première étape : elle choisit une tête. Les deux cohabiteront, l'autre pilotera du texte et des priorités. L'animation de repos, et quand la carte reprend la main ------------------------------------------------------- Sans ``screen/heartbeat`` pendant 2 s — quatre battements manqués, le contrat en demande 2 Hz — la carte considère l'orchestrateur absent et repart sur son animation : le logo, ponctué de clins d'œil. Un écran figé a l'air en panne, un écran qui cligne a l'air d'attendre. .. important:: **La reprise n'a lieu que si un heartbeat est déjà arrivé un jour.** Sans ce garde-fou, on ne distingue pas « l'orchestrateur est mort » de « personne n'en a jamais lancé » : un simple ``ros2 topic pub --once`` pour regarder une tête se faisait effacer au bout de deux secondes. L'image demandée tient donc tant que rien n'a jamais battu. Dès qu'un orchestrateur s'est manifesté, son silence redevient un signal. Animation de démarrage ~~~~~~~~~~~~~~~~~~~~~~ Toutes les images défilent au boot, avant micro-ROS, puis l'écran s'arrête sur le logo. Ce n'est pas décoratif : le défilé **valide toutes les conversions d'un coup**, et une image mal convertie se voit immédiatement. Les trois secondes ne coûtent rien — juste après, la carte attend l'agent de toute façon. .. note:: ``images_to_pages()`` **vérifie les dimensions** avant de convertir et rend ``false`` si l'image n'est pas en 128×64. Une image exportée à une autre taille produit souvent 1024 octets malgré tout : sans ce contrôle, l'écran afficherait du bruit sans que rien ne le signale. .. warning:: **L'écran est en écriture seule.** La carte route pourtant bien MISO (``PC11`` → net ``MISO1``, jusqu'au connecteur) et SPI3 est configuré en full-duplex — mais l'écran ne le pilote jamais : le SSD1309 en SPI 4 fils n'a **aucune sortie de données**, la relecture n'existe que dans les modes parallèles 6800/8080. Conséquence : ``HAL_OK`` ne prouve rien, le SPI répond exactement pareil connecteur débranché. Seul l'œil valide. Si l'écran reste noir : l'interrupteur du module doit être sur **SPI** et non I2C, puis vérifier ``RST`` (PC8), ``DC`` (PD15), ``CS`` (PD14). Et **ne pas** envoyer ``0x8D 0x14`` : c'est la pompe de charge du SSD1306. Le SSD1309 n'en a pas, son alimentation haute tension est externe. C'est le copier-coller classique depuis un exemple SSD1306. Le RatBot possède un **écran OLED monochrome 128×64** et **six boutons** (haut, bas, gauche, droite, OK, ESC), tous câblés sur la **STM32**. La STM32 garde en flash **plusieurs expressions** d'une tête de souris (normale, yeux fermés, regard, clins d'œil droit/gauche, heureuse, cœur). L'objectif : un robot **vivant et expressif**, pilotable par ROS 2 mais **robuste sans lui**. Principe directeur ------------------ **Le STM32 est un contrôleur d'écran autonome mais "bête" ; la Raspberry Pi (ROS 2) est l'orchestrateur.** La règle de partage : - ce qui doit **survivre à une panne ROS** vit dans le STM32 (animation de repos, rendu, lecture des boutons, watchdog, urgence locale) ; - ce qui est **intelligent et évolutif** vit sur la Pi (messages, priorités, menus, expressions selon l'état, actions) ; - entre les deux, un **contrat de topics** minimal, **identique en simulation et sur le robot réel** (même principe que le reste du RatBot). .. important:: **Toute la logique de priorité et de temporisation est centralisée sur la Pi.** Le STM32 est volontairement *stateless* : il affiche la dernière frame reçue (ou son repli local), sans pile d'états ni « retour à l'écran précédent ». C'est la Pi qui re-pousse la frame voulue. Firmware bête = firmware robuste. Répartition des responsabilités ------------------------------- .. list-table:: :header-rows: 1 :widths: 34 22 22 22 * - - STM32 (firmware) - Pi (ROS 2) - Simulation * - Animation de repos - **autonome** - — - émulée à l'identique * - Rendu (image, texte, menu) - **oui** - envoie des commandes - fenêtre 128×64 * - Lecture des 6 boutons - **oui** → publie - réagit aux appuis - clavier → mêmes topics * - Watchdog / « ROS ✗ » - **oui** (repli local) - envoie un *heartbeat* - idem STM32 * - Priorité / temporisation - non (stateless) - **oui** (centralisé) - non * - Messages / menus / expressions - rend ce qu'on envoie - **compose** (YAML) - rend Contrat ROS 2 (dans ``/ratbot_N/``) ----------------------------------- **ROS → STM32** - ``screen/command`` (``ratbot_msgs/ScreenCommand``) — quoi afficher ; - ``screen/heartbeat`` (``std_msgs/Empty``, ~2 Hz) — « ROS est vivant » ; son absence fait basculer le STM32 sur « ROS déconnecté » + repos. **STM32 → ROS** - ``screen/button`` (``std_msgs/UInt8``) — un appui = un identifiant (1 = haut, 2 = bas, 3 = gauche, 4 = droite, 5 = OK, 6 = ESC). **Haut niveau (phase B)** - action/service ``AskUser`` (``ratbot_msgs``) — un nœud pose une question, l'écran l'affiche, la réponse (Oui/Non, choix) est renvoyée ; - ``mission/command`` (``std_msgs/String``) — intentions déclenchées par le menu (``explore``, ``pause``, ``return_home``, ``save_map``), traitées par la logique mission (découplé de l'écran). Le message ``ScreenCommand`` ---------------------------- Premier message **custom** du projet (paquet ``ratbot_msgs``, ament_cmake). Les tailles sont **bornées** pour rester compatibles micro-ROS sur le STM32. .. code-block:: text uint8 MODE_IDLE=0 # revenir a l'animation de repos autonome uint8 MODE_FACE=1 # afficher une des 6 expressions uint8 MODE_MESSAGE=2 # titre + lignes de texte uint8 MODE_MENU=3 # lignes + curseur uint8 mode uint8 face_id # MODE_FACE : index de l'expression (0..N) uint8 LEVEL_INFO=0 uint8 LEVEL_STATE=1 uint8 LEVEL_WARN=2 uint8 LEVEL_ERROR=3 uint8 LEVEL_EMERGENCY=4 uint8 LEVEL_QUESTION=5 uint8 level string title # borne (ex. 20 caracteres) string[] lines # borne (ex. 6 lignes x 21 caracteres) int32 cursor # MODE_MENU : ligne surlignee (-1 = aucune) uint32 seq # numero de sequence (latence / cadrage) Une **question** = ``MODE_MESSAGE`` + ``level = QUESTION`` + ``lines`` (énoncé puis choix), le ``cursor`` piloté par les boutons. Un **menu** = ``MODE_MENU`` recomposé par la Pi à chaque appui. .. note:: Pas de champ ``duration`` : la **temporisation est gérée par la Pi** (elle affiche un message puis re-pousse la frame précédente). Le STM32 n'a pas à retenir « pendant combien de temps » ni « quoi ensuite ». Priorité — centralisée sur la Pi -------------------------------- La Pi choisit **une seule** frame active parmi ses sources (urgence > avertissement > menu ouvert > expression d'état > repos) et la pousse sur ``screen/command``. Le STM32 se contente alors d'un arbitrage **trivial** à trois niveaux : .. code-block:: text 1. URGENCE LOCALE (ligne e-stop cablee) <- STM32 seul, toujours prioritaire 2. derniere frame ROS recue <- tant que le heartbeat est vivant 3. animation de repos (+ banniere ROS ✗) <- defaut / ROS absent Ainsi une **urgence remplace immédiatement un menu ouvert** : soit la Pi pousse la frame d'urgence, soit le STM32 l'affiche lui-même (e-stop matériel), sans aucune logique de pile côté firmware. Robustesse (perte de communication) ----------------------------------- - **Heartbeat** Pi → STM32 (~2 Hz). Si absent au-delà d'un délai (ex. 1,5 s), le STM32 **ne reste pas figé** : bannière discrète « ROS ✗ » et retour à l'animation de repos. - Les boutons restent **lus localement** (simplement non transmis tant que ROS est absent — pas de file d'attente qui déborde). - L'**urgence locale** (ligne e-stop) reste affichable **sans ROS**. Expressions configurables (personnalisation) -------------------------------------------- L'expression du visage est un **canal de communication** piloté par ROS et **personnalisable par l'utilisateur**, dans le même esprit que la LED d'état (voir :doc:`bringup`) : un YAML éditable mappe *état/événement → expression* (+ message et niveau optionnels). .. code-block:: yaml # Les images du STM32, nommees faces: normal: 0 yeux_fermes: 1 regard: 2 clin_droit: 3 clin_gauche: 4 heureux: 5 coeur: 6 # Reaction : etat/evenement -> expression (+ message optionnel) reactions: idle: { face: __anim__ } # rend la main a l'animation autonome du STM32 exploring: { face: regard } mission_done: { face: heureux, message: "Mission terminee", level: info } distress: { face: normal, message: "Batterie faible", level: warn } emergency: { face: normal, message: "ARRET URGENCE", level: emergency } Le même ``status/state`` pilote donc **LED + visage + son** de façon cohérente, chacun personnalisable dans son YAML — sans toucher au firmware. Simulation : un émulateur OLED fidèle ------------------------------------- Un nœud **``screen_sim``** (dans ``ratbot_sim``, le « faux firmware ») reproduit le STM32 vu de ROS : - fenêtre **128×64** agrandie, rend ``screen/command`` ; - **rejoue la même animation de repos, le même watchdog et le même arbitrage** que le firmware (comportement de référence commun) ; - **clavier → ``screen/button``** (flèches, Entrée = OK, Échap = ESC) ; - embarque **6 bitmaps** de la tête de souris (copies des images du STM32). → toute l'interface ROS (messages, expressions, plus tard menus) est **testable sans hardware**, avec exactement le même contrat. ``ratbot up … screen:=true`` l'ouvre pour chaque robot simulé : l'option de ``gazebo.launch.py`` inclut ``ratbot_status/screen.launch.py`` (``screen_manager`` + ``screen_sim``) par robot. Avec ``gui:=false``, ``screen_sim`` tourne sans fenêtre et publie seulement ``screen/display``. Paquets ------- - **``ratbot_msgs``** *(ament_cmake)* : ``ScreenCommand.msg``. Réutilisé par la simu, la Pi et le firmware. - **``ratbot_status``** : nœud ``screen_manager`` (orchestrateur Pi — machine à états : alertes, expressions selon ``status/state``, **menu** navigable avec confirmation Oui/Non, heartbeat). Configs ``faces.yaml`` + ``menu.yaml``. - **``ratbot_mission``** : nœud ``mission_manager`` — consomme ``mission/command`` (émis par le menu) et exécute ``explore`` / ``save_map`` / ``return_home`` / ``pause`` ; publie ``mission/state`` et le reflète via ``status/set_state``. - **``ratbot_sim``** : nœud ``screen_sim`` (émulateur fenêtre : écran + 6 boutons de navigation + 4 SW / 4 LEDs utilisateur) + images des visages. - **Firmware STM32** : implémente le même contrat (pub/sub micro-ROS). Plan de réalisation (par phases) -------------------------------- **Phase A — le cœur expressif** (haute valeur, sans dépendance) : - ``ratbot_msgs`` minimal (``ScreenCommand``) ; - ``screen_manager`` : visages + messages qui **reflètent ``status/state``** (heureux, batterie faible, urgence…) + heartbeat ; - ``screen_sim`` : émulateur (repos autonome + watchdog + boutons). **Phase B — l'interactif** (réalisée) : - **menu** recomposé par la Pi (``menu.yaml``) : OK au repos l'ouvre, haut/bas naviguent, OK valide, ESC recule ; items *info* (batterie, IP, ID, état ROS) et *command* ; - **confirmation Oui/Non** pour les commandes sensibles (gauche/droite + OK) ; - ``mission_manager`` (``ratbot_mission``) exécute les intentions (``mission/command`` → exploration, sauvegarde de carte, retour base, pause) et reflète la mission sur le voyant/visage. Reste (plus tard) : un service/action ``AskUser`` générique pour qu'un nœud **tiers** pose une question via l'écran (le menu, lui, gère déjà ses propres confirmations en interne). Décisions de conception retenues -------------------------------- - **Menu rendu par la Pi** (contenu dynamique — IP, ID, état ROS — et extensible sans firmware) ; - **Message custom** ``ratbot_msgs`` (encoder image + texte + menu dans du ``std_msgs`` serait bricolé et peu adapté à micro-ROS) ; - **Émulateur en fenêtre** 128×64 (visuel fidèle, boutons au clavier) ; - **Boutons en ``UInt8``** (compact, adapté à l'embarqué) ; - **Heartbeat explicite** (simple et testable en simulation).