Machine à états

Comprendre l'architecture de la machine à états du micrologiciel OSSM

Architecture de la machine à états

Le micrologiciel OSSM utilise une machine à états déclarative pour gérer le comportement de l'appareil. Cela garantit des transitions prévisibles entre les modes de fonctionnement et évite les états non valides qui pourraient provoquer un comportement inattendu.

Diagramme d'état

Le diagramme suivant montre tous les états et transitions dans la machine à états OSSM :

Aperçu de la conception

La machine à états est implémentée à l'aide de Boost.SML (State Machine Language), une bibliothèque C++14 en en-tête uniquement qui fournit un langage spécifique au domaine pour définir les machines à états.

Pourquoi Boost.SML ?

Boost.SML a été choisi pour OSSM car il offre :

  • Vérification au moment de la compilation - Les transitions non valides sont détectées au moment de la compilation, pas au moment de l'exécution
  • Zéro surcharge d'exécution - Performances équivalentes à une logique switch/case écrite à la main
  • Syntaxe déclarative - La table de transition se lit comme une documentation
  • Sécurité des threads – Prise en charge intégrée de l'accès simultané via des politiques
  • Faible encombrement - En-tête unique, ~2 000 lignes de code, aucune dépendance

Syntaxe de la table de transition

Chaque ligne du tableau de transition suit ce modèle :

source_state + event [guard] / action = target_state

Par exemple :

"menu.idle"_s + buttonPress[(isOption(Menu::SimplePenetration))] = "simplePenetration"_s

Cela se lit comme suit : lorsque l'état menu.idle est actif et qu'un événement buttonPress se produit, l'état passe à simplePenetration si la garde isOption(Menu::SimplePenetration) renvoie true.

Pour un didacticiel complet sur la syntaxe des tables de transition, consultez le Tutoriel Boost.SML.

Événements

Les événements déclenchent des transitions d'état. Ce sont des structures simples définies dans Events.h qui peuvent être dispatchées depuis n'importe où dans la base de code.

ÉvénementDescriptionSource typique
ButtonPressClic sur un seul boutonBouton de l’encodeur rotatif
LongPressBouton maintenu pendant une durée prolongéeBouton de l'encodeur rotatif (maintenu)
DoublePressDeux clics rapides sur un boutonBouton de l’encodeur rotatif
DoneOpération asynchrone terminéeTâche de homing, vérification préliminaire
ErrorL'opération a échouéÉchec du homing, course trop courte
EmergencyStopArrêt immédiat requisSystèmes de sécurité
HomeDemander une séquence de homingCommande externe

Dispatch des événements

Les événements sont dispatchés à l'aide de l'instance globale stateMachine :

#include "ossm/state/state.h"

// From anywhere in the codebase
stateMachine->process_event(ButtonPress{});
stateMachine->process_event(Done{});

La machine à états est initialisée au démarrage dans state.cpp et exposée comme pointeur global.

Les événements sont définis comme des structures vides. Le type lui-même a une signification : aucune charge utile de données n’est nécessaire.

Guards

Les gardes sont des contrôles conditionnels qui déterminent si une transition doit avoir lieu. Ils renvoient true pour autoriser la transition ou false pour la bloquer.

GuardDescriptionRenvoie true lorsque ...
isOnlineVérifier la connexion WiFiL'appareil est connecté au WiFi
isUpdateAvailableRechercher les mises à jour du micrologicielLe serveur signale une version plus récente disponible
isStrokeTooShortValider le résultat du homingLa course mesurée est inférieure au seuil minimum
isOption(Menu)Vérifier l'élément de menu sélectionnéLa sélection de menu actuelle correspond à l'option spécifiée
isPreflightSafeValider la position du réglage de vitesseLe potentiomètre de vitesse est dans la zone morte (démarrage sûr)
isFirstHomedVérification unique du premier homingIl s'agit du premier homing réussi depuis le démarrage
isNotHomedVérifier l'état du homingL'appareil n'a pas effectué le homing ou celui-ci a été invalidé

Mise en œuvre des Guards

Les Guards sont définis comme des lambdas constexpr dans l'espace de noms guards. Ils appellent des fonctions d'implémentation déclarées en amont pour garder l'en-tête léger :

// In guards.h - forward declaration
bool ossmIsPreflightSafe();

namespace guards {
    // Constexpr lambda wraps the implementation
    constexpr auto isPreflightSafe = []() {
        return ossmIsPreflightSafe();
    };

    // Parameterized guard using nested lambda
    constexpr auto isOption = [](Menu option) {
        return [option]() { return ossmGetMenuOption() == option; };
    };
}

La logique réelle réside dans guards.cpp, ce qui réduit les temps de compilation et permet de modifier l’implémentation sans recompiler la table de transition.

Pour en savoir plus sur les modèles de Guard, consultez la documentation Boost.SML sur les Guards.

Actions

Les actions sont des fonctions exécutées lors des transitions d'état. Elles produisent des effets secondaires tels que la mise à jour de l'affichage, le démarrage des moteurs ou la réinitialisation des paramètres.

Actions d'affichage

ActionDescription
drawHelloAfficher l'écran de bienvenue/démarrage
(drawMenu)Rendre le menu principal
drawPlayControlsAfficher les commandes de vitesse/course/profondeur
drawPatternControlsAfficher l'interface utilisateur de sélection de motif
drawPreflightAfficher l'avertissement « réduire la vitesse pour démarrer »
drawHelpAfficher les informations d'aide/support
drawWiFiAfficher l'écran de configuration WiFi
drawUpdateAfficher l'écran « Vérification des mises à jour »
drawNoUpdateAfficher le message « le firmware est à jour »
drawUpdatingAfficher la progression de la mise à jour
drawErrorAfficher l'état d'erreur avec un message

Actions de mouvement

ActionDescription
startHomingDémarrer la séquence de homing
clearHomingRéinitialiser les variables d'état du homing
startSimplePenetrationDémarrer la tâche du mode Simple Penetration
startStrokeEngineDémarrer la tâche du mode Stroke Engine
startStreamingDémarrer la tâche du mode Streaming
emergencyStopForcer l'arrêt du moteur et désactiver les sorties

Actions de configuration

ActionDescription
resetSettingsStrokeEngineInitialiser les paramètres par défaut pour Stroke Engine (vitesse = 0, course = 50, profondeur = 10, sensation = 50)
resetSettingsSimplePenInitialiser les valeurs par défaut pour Simple Penetration (vitesse=0, course=0, profondeur=50)
incrementControlParcourir les paramètres de contrôle (course → profondeur → sensation)
setHomedMarquer l'appareil comme ayant effectué le homing avec succès
setNotHomedInvalider le homing (un nouveau homing est requis avant de pouvoir lancer un mode de mouvement)

Actions du système

ActionDescription
restartRedémarrer l'ESP32
resetWiFiEffacer les informations d'identification WiFi enregistrées
updateOSSMTélécharger et installer la mise à jour du micrologiciel

Mise en œuvre des actions

Les actions sont définies comme des lambdas constexpr dans l'espace de noms actions. Comme les Guards, elles délèguent à des fonctions d'implémentation déclarées en amont :

// In actions.h - forward declarations
void ossmDrawHello();
void ossmEmergencyStop();

namespace actions {
    constexpr auto drawHello = []() { ossmDrawHello(); };
    constexpr auto emergencyStop = []() { ossmEmergencyStop(); };
}

Les fonctions d'implémentation dans actions.cpp font appel aux modules de fonctionnalités appropriés :

// In actions.cpp
void ossmEmergencyStop() {
    stepper->forceStop();
    stepper->disableOutputs();
}

void ossmDrawHello() {
    pages::drawHello();  // Delegates to pages namespace
}

Ce modèle maintient la machine à états découplée des implémentations spécifiques et permet de développer des modules de fonctionnalités indépendamment.

Les actions ne doivent pas bloquer le thread principal. Les opérations de longue durée doivent plutôt lancer des tâches FreeRTOS.

Pour en savoir plus sur les modèles d'action, consultez la documentation sur les actions Boost.SML.

Sécurité des threads

La machine à états OSSM est configurée avec des politiques de sécurité des threads pour gérer les événements de plusieurs tâches FreeRTOS :

sml::sm<OSSMStateMachine, sml::thread_safe<ESP32RecursiveMutex>, sml::logger<StateLogger>>

Cela utilise un mutex récursif pour protéger les transitions d'état et permettre le dispatch sécurisé des événements depuis les contextes suivants :

  • La boucle principale
  • Gestionnaires d'interruptions de boutons
  • Gestionnaires de commandes BLE
  • Tâches en arrière-plan

La machine à états est initialisée dans state.cpp :

// Global state machine instance
StateMachine* stateMachine = nullptr;

void initStateMachine() {
    stateMachine = new StateMachine{};
    stateMachine->process_event(Done{});  // Trigger initial transition
}

Pour en savoir plus sur les politiques de sécurité des threads, consultez la documentation sur les politiques Boost.SML.

Journalisation d'état

Le micrologiciel comprend un StateLogger qui enregistre toutes les transitions d'état pour le débogage :

sml::logger<StateLogger>

Cela génère des informations de transition via la journalisation ESP-IDF, ce qui facilite le suivi du comportement de la machine à états pendant le développement.

Notes d'architecture

La machine à états suit une architecture modulaire découplée :

  • Définition de la machine à états (machine.h) contient uniquement la table de transition
  • Les actions et les Guards sont des lambdas constexpr qui délèguent aux fonctions d'implémentation
  • Les modules de fonctionnalités (comme pages::, simple_penetration::, stroke_engine::) contiennent la logique réelle
  • Les structures d'état globales gèrent l'état de l'application au lieu des membres de la classe

La classe OSSM dans ossm/OSSM.h est conservée pour des raisons de rétrocompatibilité avec la gestion des commandes BLE. Les nouvelles fonctionnalités doivent utiliser des fonctions d'espace de noms sans état qui fonctionnent sur l'état global.

Pour un aperçu complet de l'organisation du code source, voir Structure des dossiers.

Lectures complémentaires

Sur cette page