É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 :
Réglage |
Valeur |
Pourquoi |
|---|---|---|
|
1 |
écran monochrome |
|
8 bit |
un octet par groupe de 8 pixels |
|
yes |
donne 16 octets par ligne : la conversion en dépend |
|
top_to_bottom |
ordre des lignes |
|
forward |
bit 7 = pixel de gauche |
|
no |
on ne décompresse pas côté carte |
|
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.
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 :
./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 :
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
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
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.
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 :
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 Bringup) : un YAML éditable mappe état/événement → expression (+ message et niveau optionnels).
# 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 selonstatus/state, menu navigable avec confirmation Oui/Non, heartbeat). Configsfaces.yaml+menu.yaml.``ratbot_mission`` : nœud
mission_manager— consommemission/command(émis par le menu) et exécuteexplore/save_map/return_home/pause; publiemission/stateet le reflète viastatus/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_msgsminimal (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 dustd_msgsserait 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).