Blog

Du modèle UMT au code C++ : un contrôleur de porte en 30 minutes

Publié le 2026-05-25

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 :

Diagramme de machine à états de la porte double mode
Diagramme de machine à états de la porte double mode

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 : ManualMode et AutomaticMode
  • Quatre sous-états dans chacun : Open, Closing, Close, Opening
  • Fermeture temporisée en mode automatique via do / wait avec 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 :

Fichiers générés
Fichiers générés
FichierRôle
Door.hhDéclaration de la classe de la machine à états
Door.cppImplémentation de la machine à états
Door_DataType.hhStructure de données de la machine à états
Door_Types.hhDéclarations de types dérivés
Door_Auxilary.cpp.templateModèles pour les fonctions de garde et d'action à écrire par l'utilisateur
Door_DataType.cpp.templateModèle pour l'initialisation et la désactivation de la structure de données
Door_UserTypes.hh.templateModè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 :

Panneau de contrôle de la simulation
Panneau de contrôle de la simulation

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 ?

ExigenceApproche
Fermeture automatiquedo / wait + completion transition
Mode manuelMouvement déclenché par l'événement bouton
Changement de modeÉvénement Manual / Automatic, transitions symétriques
Changement de mode en cours de mouvementPas d'entry action sur la transition, le moteur continue
Récupération après coupure de courantpersistent 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.