Du modèle UMT au code C++ : un contrôleur de porte en 30 minutes
Les équipes qui développent des logiciels embarqués à base de machines à états se heurtent souvent à la même question : qui écrit le modèle, qui écrit le code, et comment les deux restent-ils synchronisés ?
Dans cet article, je montre la réponse qu'apporte UMTSM à cette question à travers un exemple concret. Nous transformons un contrôleur de porte, depuis un fichier .umt, en code C++ prêt pour la production — étape par étape.
Le problème : une porte fonctionnant en mode automatique et manuel
Le système doit satisfaire les exigences suivantes :
- En mode automatique, la porte attend un certain temps après s'être ouverte, puis se ferme d'elle-même.
- En mode manuel, la porte ne bouge que lorsqu'on appuie sur le bouton.
- Le passage d'un mode à l'autre est possible à l'exécution.
- Un changement de mode pendant l'ouverture ou la fermeture n'interrompt pas le mouvement.
- Après une coupure de courant, le système mémorise son dernier état — au redémarrage, il reprend là où il s'était arrêté.
Ce sont de vraies exigences produit. Maintenir ces cinq points proprement dans une FSM écrite à la main peut prendre des semaines. Si l'on exprime le système sous la forme d'un diagramme UML approximatif :
Exprimé en UMTSM, le fichier .umt ressemble à ceci :
Étape 1 : Modéliser la machine à états
type sm Engine;
sm Door
{
persistent deep history -> ManualMode_Open;
state ManualMode
{
Open:
ButtonPressed -> Closing / engineRunACCW;
Closing:
entry / engineRunACCW;
DoorClosed / engineStop -> Close;
ButtonPressed / engineStop -> Opening;
Close:
ButtonPressed -> Opening;
Opening:
entry / engineRunCCW;
DoorOpen / engineStop -> Open;
ButtonPressed / engineStop -> Closing;
Automatic -> AutomaticMode;
}
state AutomaticMode
{
Open:
entry / resetWaitingTime;
do / wait;
ButtonPressed / resetWaitingTime;
-> Closing;
Closing:
entry / engineRunACCW;
DoorClosed / engineStop -> Close;
ButtonPressed / engineStop -> Opening;
Close:
ButtonPressed -> Opening;
Opening:
entry / engineRunCCW;
DoorOpen / engineStop -> Open;
Manual -> ManualMode;
}
}
Quelques lignes suffisent à définir :
- Deux états composites de haut niveau :
ManualModeetAutomaticMode - Quatre sous-états dans chacun :
Open,Closing,Close,Opening - Fermeture temporisée en mode automatique via
do / waitavec une completion transition - Récupération après coupure de courant via
persistent deep history - Déclaration de dépendance envers la machine à états Engine via
type sm Engine
Étape 2 : Génération du code
UMTSM analyse le modèle et le convertit en une représentation interne (IR) hiérarchique. CppGen génère les fichiers suivants à partir de cet IR :

| Fichier | Rôle |
|---|---|
| Door.hh | Déclaration de la classe de la machine à états |
| Door.cpp | Implémentation de la machine à états |
| Door_DataType.hh | Structure de données de la machine à états |
| Door_Types.hh | Déclarations de types dérivés |
| Door_Auxilary.cpp.template | Modèles pour les fonctions de garde et d'action à écrire par l'utilisateur |
| Door_DataType.cpp.template | Modèle pour l'initialisation et la désactivation de la structure de données |
| Door_UserTypes.hh.template | Modèle pour les déclarations de types externes définis par l'utilisateur |
Les fichiers générés ne doivent pas être modifiés — ils sont réécrits à chaque nouvelle génération. Les fichiers appartenant à l'utilisateur vivent sous src/door/ et ne sont pas affectés par la génération.
Étape 3 : Implémenter les actions
En s'appuyant sur le modèle généré, nous créons src/door/Door_Auxilary.cpp :
void Door::engineRunCCW([[maybe_unused]] Door_DataType const& input)
{
instanceData.doorActionTimeStart = std::chrono::system_clock::now();
instanceData.pEngine->trigger_runCCW();
}
void Door::engineRunACCW([[maybe_unused]] Door_DataType const& input)
{
instanceData.doorActionTimeStart = std::chrono::system_clock::now();
instanceData.pEngine->trigger_runACCW();
}
void Door::engineStop([[maybe_unused]] Door_DataType const& input)
{
instanceData.pEngine->trigger_stop();
}
void Door::resetWaitingTime([[maybe_unused]] Door_DataType const& input)
{
instanceData.waitUntil =
std::chrono::system_clock::now() + DOOR_AUTO_CLOSE_DELAY;
}
void Door::wait([[maybe_unused]] Door_DataType const& input)
{
std::this_thread::sleep_until(instanceData.waitUntil);
}
Les actions sont des méthodes virtual — différentes implémentations matérielles peuvent être fournies sans toucher au squelette de la SM. En simulation, le moteur est imité par un thread en veille ; sur le matériel réel, une sortie GPIO est pilotée à la place.
Étape 4 : Assembler le système
Dans src/main/main.cpp, toutes les machines à états sont instanciées et interconnectées :
Door door;
Engine engine;
Button button;
// ...
door.instanceData.pEngine = &engine;
button.instanceData.pDoor = &door;
engine.start();
door.start(); // l'historique persistant est chargé ici
button.start();
// ...
Lors de l'appel de door.start(), load_Deep_Main() lit le dernier état depuis le stockage persistant. Au tout premier démarrage, le système commence par défaut à ManualMode::Open. Aux démarrages suivants, le dernier état actif est restauré.
Étape 5 : Exécuter
cmake -S . -B build/Release -DCMAKE_BUILD_TYPE=Release
cmake --build build/Release
./build/Release/src/main/door
L'application s'ouvre avec une interface ncurses plein écran :

Appuyer sur A bascule le système en mode automatique. La porte s'ouvre, le minuteur démarre, et à l'expiration du délai la fermeture commence. Appuyer sur M entre-temps laisse la porte poursuivre son mouvement en cours — le moteur ne s'arrête pas — mais à partir de ce moment les règles du mode manuel s'appliquent.
Qu'avons-nous obtenu ?
| Exigence | Approche |
|---|---|
| Fermeture automatique | do / wait + completion transition |
| Mode manuel | Mouvement déclenché par l'événement bouton |
| Changement de mode | Événement Manual / Automatic, transitions symétriques |
| Changement de mode en cours de mouvement | Pas d'entry action sur la transition, le moteur continue |
| Récupération après coupure de courant | persistent deep history + store_Deep_Main |
Code écrit à la main au total : ~150 lignes d'implémentations d'actions et ~50 lignes de main.cpp. Tout le reste a été généré.
Conclusion
L'essence de ce que UMTSM apporte dans cet exemple tient en une phrase : dériver à la fois le modèle et le code du même fichier source. Lorsque le fichier .umt change, le squelette généré est mis à jour et les implémentations utilisateur sont préservées. Le modèle et le code ne perdent jamais leur synchronisation.
Le code complet de l'exemple est disponible sur GitHub : https://github.com/demiralp/umtsm-examples-cpp
Pour toute question ou tout projet lié à UMTSM, n'hésitez pas à prendre contact.
