É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

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.

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 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).