GRAFCET — Ancienne version (archive)
Cette page conserve les anciennes versions du moteur GRAFCET et des exemples, pour référence historique. Voir la version de référence actuelle →
C'est quoi le GRAFCET ?
Le GRAFCET (Graphe Fonctionnel de Commande Étape/Transition) est un standard français normalisé IEC 60848, utilisé dans toute l'industrie pour programmer des automates séquentiels. Tu le retrouves dans les ascenseurs, les feux tricolores, les machines-outils, les chaînes de montage.
Un système séquentiel passe d'un état stable à un autre selon des conditions logiques. Le GRAFCET modélise ça avec deux éléments fondamentaux :
□ Étape
Un état stable du système. Quand une étape est active, ses actions s'exécutent en continu. Plusieurs étapes peuvent être simultanément actives (divergence ET = parallélisme).
— Transition
Une condition logique entre deux étapes. Quand la réceptivité est vraie et l'étape source est active, la transition est franchie.
Le cycle d'exécution normalisé
Un automate GRAFCET tourne en boucle infinie selon un cycle en 7 phases séquentielles. Respecter cet ordre garantit un comportement déterministe — chaque phase lit le résultat de la précédente :
| Phase | Fonction | Ce qu'elle fait |
|---|---|---|
| franchir(T, trans) | Évolution | Reset des fronts d'étape, puis active/désactive les étapes selon les transitions validées |
| tick() | Timers | Incrémente les compteurs de temps des étapes actives |
| gerer_actions() | Actions | Calcule les variables logiques selon les étapes actives — les fronts rising/falling sont lisibles ici |
| affecter_sorties() | Sorties | Applique les variables sur le matériel (LEDs, moteurs...) |
| lire_entrees() | Entrées | Lit les capteurs et boutons |
| detecter_fronts_entrees() | Fronts d'entrée | Détecte les changements d'état des capteurs — fm[i] / fd[i] (optionnel) |
| calculer_transitions() | Transitions | Évalue les conditions de franchissement |
⚠️ Pourquoi cet ordre ?
franchir() est en début de cycle : il pose les fronts d'étape (rising/falling) qui restent visibles par gerer_actions() dans le même cycle. Les sorties sont affectées avant la lecture des entrées pour éviter qu'une sortie activée dans le même cycle influence une entrée — comportement non-déterministe à éviter dans un automate industriel.
Fichiers commentés
Chaque fichier est précédé de son contexte. Le code est affiché directement — bouton copier + lien GitHub disponibles.
⚙️ Le moteur GRAFCET
La classe réutilisable — indépendante du matériel, utilisable dans n'importe quel projet.
1 La classe Grafcet — étapes, timers, fronts montants/descendants
La classe Grafcet encapsule tout l'état interne du séquenceur. Elle ne connaît aucun composant matériel — pas de Pin, pas de LED, pas de capteur. Elle ne manipule que des listes de booléens et d'entiers. C'est ce qui la rend réutilisable dans n'importe quel projet.
La table de transitions T décrit la structure de l'automate sous forme de données :
T = [
(0, (0,), (1,)), # T0 : désactive étape 0, active étape 1
(1, (1,), (2,)), # T1 : désactive étape 1, active étape 2
(2, (2,), (0,)), # T2 : désactive étape 2, active étape 0
] Les fronts d'étape (rising / falling) sont True pendant un seul cycle, au moment précis où une étape s'active ou se désactive — équivalent du StepRising / StepFalling des PLCs industriels.
g.etapes[i]— True si l'étape i est activeg.tempo[i]— durée en ms depuis l'activation (remis à 0 à la désactivation)g.rising[i]— True pendant 1 cycle au moment de l'activationg.falling[i]— True pendant 1 cycle au moment de la désactivation
Les fronts d'entrée (optionnels, avec nb_entrees) détectent le changement d'état des capteurs — utile pour distinguer "bouton pressé" de "bouton maintenu" :
g.entrees[i]— état courant de l'entrée (à remplir danslire_entrees())g.fm[i]— front montant : True pendant 1 cycle quand l'entrée passe de False à Trueg.fd[i]— front descendant : True pendant 1 cycle quand l'entrée passe de True à False
# Exemple : ascenseur avec front montant sur le bouton Start
g = Grafcet(nb_etapes=3, nb_fronts=1)
def lire_entrees():
g.entrees[0] = bpA.value() # état brut du bouton Start
def calculer_transitions():
trans[0] = g.etapes[0] and g.fm[0] # front montant de bpA
# → ne franchit qu'à l'APPUI, pas si le bouton reste enfoncé grafcet.py — Moteur GRAFCET réutilisable .python avance/grafcet/ancien/grafcet.py 199 lignes
GitHub
# =============================================================================
# grafcet.py — Moteur GRAFCET pour MicroPython
# =============================================================================
# Implémentation du standard IEC 60848 (GRAFCET / Sequential Function Chart)
# Auteur : Vincent — Fablab ESP32 Ardèche
#
# PRINCIPE DU GRAFCET :
# Un GRAFCET est une machine à états séquentielle composée de :
# - étapes : états stables du système (actives ou inactives)
# - actions : ce que fait le système quand une étape est active
# - transitions : conditions logiques pour passer d'une étape à une autre
# - réceptivités : la condition associée à chaque transition
#
# CYCLE D'EXÉCUTION NORMALISÉ (à respecter dans la boucle principale) :
# 1. franchir(T, trans) → franchissement des transitions validées
# (reset des fronts d'étape, puis pose des nouveaux)
# 2. tick() → mise à jour des timers
# 3. gerer_actions() → calcul des actions selon étapes actives
# (les fronts d'étape rising/falling sont lisibles ici)
# 4. affecter_sorties() → application des actions sur les sorties physiques
# 5. lire_entrees() → lecture des capteurs et boutons
# 6. detecter_fronts_entrees() → détection des fronts montants/descendants d'entrée
# 7. calculer_transitions() → évaluation des conditions de transition
#
# NOTE : franchir() est en DÉBUT de cycle (pas en fin) pour que les fronts
# d'étape qu'il pose soient visibles par gerer_actions() du même cycle.
# =============================================================================
class Grafcet:
"""
Moteur d'exécution GRAFCET générique pour MicroPython.
Usage sans fronts d'entrée :
g = Grafcet(nb_etapes=3)
while True:
g.franchir(T, transitions)
g.tick(20)
gerer_actions()
affecter_sorties()
lire_entrees()
calculer_transitions()
synchro_ms(20)
Usage avec fronts d'entrée (nb_fronts = nombre de boutons/capteurs à surveiller) :
g = Grafcet(nb_etapes=3, nb_fronts=2)
while True:
g.franchir(T, transitions)
g.tick(20)
gerer_actions()
affecter_sorties()
lire_entrees() # → remplir g.entrees[i]
g.detecter_fronts_entrees() # → calcule g.fm[i] / g.fd[i]
calculer_transitions() # → utiliser g.fm[i] pour les fronts
synchro_ms(20)
"""
def __init__(self, nb_etapes, etape_initiale=0, nb_fronts=0):
"""
Initialise le GRAFCET.
:param nb_etapes: nombre total d'étapes (taille des tableaux internes)
:param nb_fronts: nombre d'entrées à surveiller pour les fronts
:param etape_initiale: indice de l'étape active au démarrage (défaut : 0)
"""
# --- Tableau d'activation des étapes ---
# etapes[i] = True → l'étape i est active
# etapes[i] = False → l'étape i est inactive
# Plusieurs étapes peuvent être simultanément actives (divergence ET)
self.etapes = [False] * nb_etapes
# --- Timers par étape (en millisecondes) ---
# tempo[i] s'incrémente automatiquement tant que l'étape i est active
# tempo[i] est remis à 0 lors de la désactivation de l'étape
# Utilisation : transitions temporisées ex: tempo[0] > 500 (500 ms)
self.tempo = [0] * nb_etapes
# --- Compteurs par étape ---
# compt[i] peut être incrémenté manuellement dans gerer_actions()
# compt[i] est remis à 0 lors de la désactivation de l'étape
# Utilisation : compter des événements pendant qu'une étape est active
self.compt = [0] * nb_etapes
# --- Fronts montants (rising edge) ---
# rising[i] = True pendant UN SEUL cycle, au moment où l'étape i s'active
# Équivalent du StepRising dans les PLCs industriels
# Utilisation : déclencher une action UNE SEULE FOIS à l'entrée d'une étape
self.rising = [False] * nb_etapes
# --- Fronts descendants (falling edge) ---
# falling[i] = True pendant UN SEUL cycle, au moment où l'étape i se désactive
# Équivalent du StepFalling dans les PLCs industriels
# Utilisation : déclencher une action UNE SEULE FOIS à la sortie d'une étape
self.falling = [False] * nb_etapes
# Activation de l'étape initiale : seule étape active au démarrage
self.etapes[etape_initiale] = True
# --- Fronts d'entrée ---
# Détection des changements d'état des capteurs/boutons
# fm[i] = front montant d'entrée i (True pendant 1 cycle quand entrée passe False→True)
# fd[i] = front descendant d'entrée i (True pendant 1 cycle quand entrée passe True→False)
self.nb_fronts = nb_fronts
self.entrees = [False] * nb_fronts # état actuel (à remplir dans lire_entrees())
self.entrees_prec = [False] * nb_fronts # état du cycle précédent
self.fm = [False] * nb_fronts # fronts montants d'entrée
self.fd = [False] * nb_fronts # fronts descendants d'entrée
# -------------------------------------------------------------------------
def tick(self, dt_ms=20):
"""
Met à jour les timers internes. À appeler UNE FOIS par cycle de boucle,
APRÈS franchir() et AVANT gerer_actions().
- Incrémente tempo[i] pour chaque étape active
- Ne touche PAS aux fronts (rising/falling) — ceux-ci sont gérés par franchir()
:param dt_ms: durée du cycle en millisecondes — doit correspondre
à la valeur passée à synchro_ms() dans la boucle principale
"""
# Incrémente le timer de chaque étape actuellement active
for i in range(len(self.etapes)):
if self.etapes[i]:
self.tempo[i] += dt_ms # ex: après 10 cycles à 20ms → tempo[i] = 200
# -------------------------------------------------------------------------
def franchir(self, T, transitions):
"""
Franchit les transitions validées et met à jour le tableau des étapes.
Parcourt la table de transitions T. Pour chaque transition dont la
réceptivité est True, désactive les étapes sources et active les étapes cibles.
:param T: table de transitions — liste de tuples de la forme :
(indice_transition, (étapes_à_désactiver,), (étapes_à_activer,))
Exemple pour un GRAFCET à 3 étapes en boucle :
T = [
(0, (0,), (1,)), # transition 0 : étape 0 → étape 1
(1, (1,), (2,)), # transition 1 : étape 1 → étape 2
(2, (2,), (0,)), # transition 2 : étape 2 → étape 0
]
Pour une divergence ET (parallélisme) :
(0, (0,), (1, 2)), # transition 0 active étapes 1 ET 2 simultanément
:param transitions: liste de booléens — transitions[i] = True si la
réceptivité de la transition i est satisfaite
"""
# Reset des fronts d'étape AVANT de poser les nouveaux
# Les fronts posés par le cycle précédent ont été lus par gerer_actions()
self.rising = [False] * len(self.etapes)
self.falling = [False] * len(self.etapes)
# Parcourt chaque règle de la table de transitions
for t_id, desactiver, activer in T:
# Ne franchit que si la condition de cette transition est vraie
if transitions[t_id]:
# Désactivation des étapes sources
for s in desactiver:
self.falling[s] = True # signale le front descendant pour ce cycle
self.etapes[s] = False # désactive l'étape
self.tempo[s] = 0 # remet le timer à zéro (prêt pour la prochaine activation)
self.compt[s] = 0 # remet le compteur à zéro
# Activation des étapes cibles
for s in activer:
self.rising[s] = True # signale le front montant pour ce cycle
self.etapes[s] = True # active l'étape
# -------------------------------------------------------------------------
def detecter_fronts_entrees(self):
"""
Détecte les fronts montants et descendants des entrées.
À appeler APRÈS lire_entrees() et AVANT calculer_transitions().
Compare self.entrees (état actuel, rempli par l'utilisateur dans
lire_entrees()) avec self.entrees_prec (état du cycle précédent).
Résultat :
- self.fm[i] = True si l'entrée i vient de passer de False à True
- self.fd[i] = True si l'entrée i vient de passer de True à False
"""
for i in range(self.nb_fronts):
self.fm[i] = self.entrees[i] and not self.entrees_prec[i]
self.fd[i] = not self.entrees[i] and self.entrees_prec[i]
self.entrees_prec[i] = self.entrees[i]
🏗️ L'ascenseur simulé
Exemple complet sur carte ENIM — 3 étapes, 3 transitions, LEDs de statut.
1 Ascenseur avec LEDs — version de base
L'ascenseur est l'exemple pédagogique classique du GRAFCET. Trois étapes, trois transitions — une logique qui se lit comme un schéma :
Étape 0 — Repos │ T0 : bpA AND tempo[0] > 200 ms
Étape 1 — Descente │ T1 : bpC OR niveau ≤ -99
Étape 2 — Montée │ T2 : bpB OR niveau ≥ -1
└──────────────────► Étape 0 Le niveau simulé (0 = haut, -100 = bas) remplace les fins de course physiques. Les LEDs de la carte ENIM indiquent l'étape active. Version minimaliste : seuls grafcet.py et essential.py sont nécessaires.
Fichiers nécessaires sur l'ESP32 : grafcet.py, essential.py (sans OLED).
ascenseur_enim_led.py — Ascenseur ENIM avec LEDs .python avance/grafcet/ancien/ascenseur_enim_led.py 170 lignes
GitHub
# =============================================================================
# ascenseur_enim_led.py — Simulation d'ascenseur sur carte ENIM (sans OLED)
# =============================================================================
# Version allégée : affichage uniquement sur le bandeau NeoPixel.
#
# CORRESPONDANCE CARTE ENIM ↔ ASCENSEUR :
#
# Entrées :
# bpA (Pin 25) → bouton Start
# bpB (Pin 34) → fin de course HAUT
# bpC (Pin 39) → fin de course BAS
#
# Sorties :
# led_bleue (Pin 2) → témoin étape 0 (repos)
# led_verte (Pin 18) → commande Descente
# led_jaune (Pin 19) → commande Montée
# np (Pin 26) → indicateur de niveau (barre NeoPixel 8 LEDs)
# Pin 12 → sortie réelle Descente (driver moteur / relais)
# Pin 13 → sortie réelle Montée (driver moteur / relais)
#
# GRAFCET (3 étapes) :
#
# ┌──────────────────────────────────────┐
# │ ÉTAPE 0 — Repos │ led_bleue allumée
# └──────────────────┬───────────────────┘
# │ T0 : bpA pressé ET tempo[0] > 200 ms
# ┌──────────────────▼───────────────────┐
# │ ÉTAPE 1 — Descente │ led_verte allumée
# └──────────────────┬───────────────────┘
# │ T1 : bpC actif OU niveau simulé ≤ -99
# ┌──────────────────▼───────────────────┐
# │ ÉTAPE 2 — Montée │ led_jaune allumée
# └──────────────────┬───────────────────┘
# │ T2 : bpB actif OU niveau simulé ≥ -1
# └────────────────────────────► ÉTAPE 0
# =============================================================================
from machine import Pin
from grafcet import Grafcet
from essential import (
synchro_ms, # synchronisation cycle 20 ms
bpA, # bouton Start (Pin 25)
bpB, # fin de course HAUT (Pin 34)
bpC, # fin de course BAS (Pin 39)
led_bleue, # témoin repos (Pin 2)
led_verte, # commande Descente (Pin 18)
led_jaune, # commande Montée (Pin 19)
np, # NeoPixel 8 LEDs (Pin 26)
)
# Broches libres pour un vrai moteur (optionnel)
sortie_descente = Pin(12, Pin.OUT) # driver moteur / relais Descente
sortie_montee = Pin(13, Pin.OUT) # driver moteur / relais Montée
# Sécurité : sorties moteur à 0 au démarrage
sortie_descente.value(0)
sortie_montee.value(0)
# NeoPixel éteint au démarrage
for led in range(8):
np[led] = (0, 0, 0)
np.write()
# =============================================================================
# INITIALISATION DU MOTEUR GRAFCET
# =============================================================================
g = Grafcet(nb_etapes=3, etape_initiale=0)
T = [
(0, (0,), (1,)), # T0 : Repos → Descente
(1, (1,), (2,)), # T1 : Descente → Montée
(2, (2,), (0,)), # T2 : Montée → Repos
]
# =============================================================================
# VARIABLES
# =============================================================================
Descendre = False
Monter = False
Start = False
Haut = False
Bas = False
niveau = 0 # position simulée : 0 = haut, -100 = bas
vitesse = 1 # déplacement par cycle
x_ancien = 0 # dernière position NeoPixel affichée
transitions = [False] * len(T)
# =============================================================================
# SIMULATION DU NIVEAU — NeoPixel uniquement
# =============================================================================
def ascenseur(inc):
"""
Déplace la cabine simulée et met à jour le bandeau NeoPixel.
:param inc: -vitesse = descente, +vitesse = montée
"""
global niveau, x_ancien
# Déplacement avec butées
niveau = niveau + inc
if niveau < -100: niveau = -100 # plancher
if niveau > 0: niveau = 0 # plafond
# Conversion en position NeoPixel (0 = haut, 7 = bas)
x = abs(int(-niveau / 12.6))
# Rafraîchissement uniquement si la position a changé
if x != x_ancien:
for led in range(0, x): np[led] = (0, 30, 0) # sous la cabine : vert
for led in range(x, 8): np[led] = (0, 0, 0) # au-dessus : éteint
np[x] = (0, 50, 0) # position cabine : vert vif
np.write()
x_ancien = x
# =============================================================================
# CYCLE GRAFCET
# =============================================================================
def gerer_actions():
global Descendre, Monter
if g.etapes[0]: Descendre = False ; Monter = False # Repos
if g.etapes[1]: Descendre = True ; Monter = False # Descente
if g.etapes[2]: Descendre = False ; Monter = True # Montée
def affecter_sorties():
led_bleue.value(g.etapes[0]) # témoin repos
led_verte.value(Descendre) # LED descente
led_jaune.value(Monter) # LED montée
sortie_descente.value(Descendre) # sortie moteur Descente
sortie_montee.value(Monter) # sortie moteur Montée
if Descendre: ascenseur(-vitesse) # simulation descente
if Monter: ascenseur(+vitesse) # simulation montée
def lire_entrees():
global Start, Haut, Bas
Start = bpA.value()
Bas = bpC.value() or (niveau <= -99) # capteur OU butée simulée
Haut = bpB.value() or (niveau >= -1) # capteur OU butée simulée
def calculer_transitions():
transitions[0] = g.etapes[0] and Start and (g.tempo[0] > 200) # T0 : départ
transitions[1] = g.etapes[1] and Bas # T1 : fond atteint
transitions[2] = g.etapes[2] and Haut # T2 : haut atteint
# =============================================================================
# BOUCLE PRINCIPALE
# =============================================================================
while True:
g.franchir(T, transitions)
g.tick(20)
gerer_actions()
affecter_sorties()
lire_entrees()
calculer_transitions()
synchro_ms(20)
2 Ascenseur avec front montant — détection d'appui
Même ascenseur, mais le bouton Start est traité en front montant au lieu d'un simple niveau logique. La différence est visible quand on maintient le bouton enfoncé :
SANS front (version de base)
Si on maintient bpA, l'ascenseur redémarre automatiquement à chaque retour en étape 0.
AVEC front (cette version)
Il faut relâcher puis ré-appuyer pour relancer un cycle. Un seul appui = un seul cycle.
Ce qui change dans le code :
g = Grafcet(nb_etapes=3, nb_fronts=1) # 1 front à surveiller
g.entrees[0] = bpA.value() # état brut dans lire_entrees()
g.detecter_fronts_entrees() # phase 6 de la boucle
transitions[0] = g.etapes[0] and g.fm[0] # front montant au lieu de niveau g.fm[0] n'est True que pendant 1 seul cycle, à l'instant précis où le bouton passe de relâché à enfoncé. Même si on maintient le bouton, fm[0] retombe à False au cycle suivant.
Fichiers nécessaires sur l'ESP32 : grafcet.py, essential.py (sans OLED).
ascenseur_enim_led_front.py — Ascenseur ENIM avec front montant .python avance/grafcet/ancien/ascenseur_enim_led_front.py 198 lignes
GitHub
# =============================================================================
# ascenseur_enim_led_front.py — Ascenseur avec détection de front montant
# =============================================================================
# Même ascenseur que ascenseur_enim_led.py, mais le bouton Start (bpA)
# est traité en FRONT MONTANT au lieu d'un simple niveau logique.
#
# POURQUOI ?
# Sans front : si on maintient bpA enfoncé, l'ascenseur redémarre
# automatiquement à chaque retour en étape 0 (la condition reste vraie).
# Avec front : il faut RELÂCHER puis RE-APPUYER pour relancer un cycle.
# Seul l'instant précis de l'appui déclenche la transition.
#
# PRINCIPE :
# Niveau = lire l'état du bouton (True tant qu'il est enfoncé)
# Front = détecter le CHANGEMENT d'état (True pendant 1 seul cycle)
#
# fm[i] = front montant : bouton relâché → enfoncé (1 cycle)
# fd[i] = front descendant : bouton enfoncé → relâché (1 cycle)
#
# CE QUI CHANGE PAR RAPPORT À ascenseur_enim_led.py :
# 1. Grafcet(..., nb_fronts=1) → réserve 1 front à surveiller
# 2. g.entrees[0] = bpA.value() → on écrit l'état brut dans lire_entrees()
# 3. g.detecter_fronts_entrees() → on appelle dans la boucle (phase 6)
# 4. g.fm[0] dans calculer_transitions → on utilise le front au lieu du niveau
# (remplace : Start and tempo[0] > 200)
#
# CORRESPONDANCE CARTE ENIM ↔ ASCENSEUR :
#
# Entrées :
# bpA (Pin 25) → bouton Start (front montant détecté)
# bpB (Pin 34) → fin de course HAUT
# bpC (Pin 39) → fin de course BAS
#
# Sorties :
# led_bleue (Pin 2) → témoin étape 0 (repos)
# led_verte (Pin 18) → commande Descente
# led_jaune (Pin 19) → commande Montée
# np (Pin 26) → indicateur de niveau (barre NeoPixel 8 LEDs)
# Pin 12 → sortie réelle Descente (driver moteur / relais)
# Pin 13 → sortie réelle Montée (driver moteur / relais)
#
# GRAFCET (3 étapes) :
#
# ┌──────────────────────────────────────┐
# │ ÉTAPE 0 — Repos │ led_bleue allumée
# └──────────────────┬───────────────────┘
# │ T0 : front montant de bpA (↑)
# ┌──────────────────▼───────────────────┐
# │ ÉTAPE 1 — Descente │ led_verte allumée
# └──────────────────┬───────────────────┘
# │ T1 : bpC actif OU niveau simulé ≤ -99
# ┌──────────────────▼───────────────────┐
# │ ÉTAPE 2 — Montée │ led_jaune allumée
# └──────────────────┬───────────────────┘
# │ T2 : bpB actif OU niveau simulé ≥ -1
# └────────────────────────────► ÉTAPE 0
#
# Fichiers nécessaires sur l'ESP32 :
# grafcet.py (moteur GRAFCET)
# essential.py (déclarations carte ENIM — sans OLED)
# =============================================================================
from machine import Pin
from grafcet import Grafcet
from essential import (
synchro_ms, # synchronisation cycle 20 ms
bpA, # bouton Start (Pin 25)
bpB, # fin de course HAUT (Pin 34)
bpC, # fin de course BAS (Pin 39)
led_bleue, # témoin repos (Pin 2)
led_verte, # commande Descente (Pin 18)
led_jaune, # commande Montée (Pin 19)
np, # NeoPixel 8 LEDs (Pin 26)
)
# Broches libres pour un vrai moteur (optionnel)
sortie_descente = Pin(12, Pin.OUT) # driver moteur / relais Descente
sortie_montee = Pin(13, Pin.OUT) # driver moteur / relais Montée
# Sécurité : sorties moteur à 0 au démarrage
sortie_descente.value(0)
sortie_montee.value(0)
# NeoPixel éteint au démarrage
for led in range(8):
np[led] = (0, 0, 0)
np.write()
# =============================================================================
# INITIALISATION DU MOTEUR GRAFCET
# =============================================================================
# nb_fronts=1 : on surveille le front montant de bpA (entrée 0)
g = Grafcet(nb_etapes=3, nb_fronts=1, etape_initiale=0)
T = [
(0, (0,), (1,)), # T0 : Repos → Descente
(1, (1,), (2,)), # T1 : Descente → Montée
(2, (2,), (0,)), # T2 : Montée → Repos
]
# =============================================================================
# VARIABLES
# =============================================================================
Descendre = False
Monter = False
Haut = False
Bas = False
niveau = 0 # position simulée : 0 = haut, -100 = bas
vitesse = 1 # déplacement par cycle
x_ancien = 0 # dernière position NeoPixel affichée
transitions = [False] * len(T)
# =============================================================================
# SIMULATION DU NIVEAU — NeoPixel uniquement
# =============================================================================
def ascenseur(inc):
"""
Déplace la cabine simulée et met à jour le bandeau NeoPixel.
:param inc: -vitesse = descente, +vitesse = montée
"""
global niveau, x_ancien
# Déplacement avec butées
niveau = niveau + inc
if niveau < -100: niveau = -100 # plancher
if niveau > 0: niveau = 0 # plafond
# Conversion en position NeoPixel (0 = haut, 7 = bas)
x = abs(int(-niveau / 12.6))
# Rafraîchissement uniquement si la position a changé
if x != x_ancien:
for led in range(0, x): np[led] = (0, 30, 0) # sous la cabine : vert
for led in range(x, 8): np[led] = (0, 0, 0) # au-dessus : éteint
np[x] = (0, 50, 0) # position cabine : vert vif
np.write()
x_ancien = x
# =============================================================================
# CYCLE GRAFCET
# =============================================================================
def gerer_actions():
global Descendre, Monter
if g.etapes[0]: Descendre = False ; Monter = False # Repos
if g.etapes[1]: Descendre = True ; Monter = False # Descente
if g.etapes[2]: Descendre = False ; Monter = True # Montée
def affecter_sorties():
led_bleue.value(g.etapes[0]) # témoin repos
led_verte.value(Descendre) # LED descente
led_jaune.value(Monter) # LED montée
sortie_descente.value(Descendre) # sortie moteur Descente
sortie_montee.value(Monter) # sortie moteur Montée
if Descendre: ascenseur(-vitesse) # simulation descente
if Monter: ascenseur(+vitesse) # simulation montée
def lire_entrees():
global Haut, Bas
g.entrees[0] = bpA.value() # état brut du bouton Start
Bas = bpC.value() or (niveau <= -99) # capteur OU butée simulée
Haut = bpB.value() or (niveau >= -1) # capteur OU butée simulée
def calculer_transitions():
# T0 : g.fm[0] = front montant de bpA → True uniquement à l'APPUI
# (même si on maintient le bouton, ça ne redémarre pas)
transitions[0] = g.etapes[0] and g.fm[0]
transitions[1] = g.etapes[1] and Bas # T1 : fond atteint
transitions[2] = g.etapes[2] and Haut # T2 : haut atteint
# =============================================================================
# BOUCLE PRINCIPALE
# =============================================================================
while True:
g.franchir(T, transitions)
g.tick(20)
gerer_actions()
affecter_sorties()
lire_entrees()
g.detecter_fronts_entrees() # détecte fm[0] / fd[0] de bpA
calculer_transitions()
synchro_ms(20)
🔬 Variante conforme IEC 60848
Version expérimentale — en cours de validation. Trois améliorations par rapport au moteur de base.
⚠️ Version en cours de validation
Ces fichiers sont des variantes expérimentales. Les versions stables restent grafcet.py et ascenseur_enim_led.py.
1 Moteur GRAFCET variante — 3 corrections IEC 60848
Correction 1 — Validation automatique des transitions (Règle 2)
Le moteur vérifie lui-même que toutes les étapes sources d'une transition sont actives. L'utilisateur n'écrit que la réceptivité pure dans calculer_transitions() :
grafcet.py (version stable)
transitions[0] = g.etapes[0] and Start and (g.tempo[0] > 200) grafcet_variante.py
transitions[0] = Start and (g.tempo[0] > 200) Correction 2 — Franchissement simultané en deux passes (Règle 4)
Toutes les transitions simultanément franchissables sont franchies ensemble. Le moteur collecte d'abord, applique ensuite :
- Passe 1 : parcourt toute la table T, collecte les étapes à désactiver et à activer
- Règle 5 : si une étape apparaît dans les deux ensembles (conflit), elle reste active — son tempo et son compteur ne sont pas remis à 0
- Passe 2 : applique toutes les désactivations puis toutes les activations
Correction 3 — Modes de sortie documentés (§3.2)
Deux modes d'action sur les sorties, à ne jamais mélanger pour une même variable :
Mode CONTINU (assignation)
Quand : la sortie est liée à une seule étape. Active = ON, inactive = OFF.
Sécurité : si le programme plante, les sorties passent à 0 (état sûr).
Descendre = g.etapes[1] # moteur
led.value(g.etapes[0]) # témoin Mode MÉMORISÉ (SET/RESET)
Quand : la sortie traverse plusieurs étapes — SET dans une étape, RESET dans une autre.
Attention : si le programme plante entre SET et RESET, la sortie reste dans son dernier état.
if g.rising[1]: alarme = True # SET
if g.falling[2]: alarme = False # RESET Règle simple : sortie liée à 1 étape → continu. Sortie qui traverse plusieurs étapes → mémorisé. Dans l'exemple de l'ascenseur, les LEDs verte/jaune et les moteurs sont en continu (1 étape chacun). La LED rouge clignotante est en mémorisé (SET à l'étape Descente, RESET à la fin de l'étape Montée — elle couvre 2 étapes).
Étapes initiales multiples (Règle 1)
etape_initiale accepte un entier ou une liste. Utile quand plusieurs branches parallèles doivent démarrer actives :
g = Grafcet(nb_etapes=6, etape_initiale=[0, 3]) # étapes 0 et 3 actives au démarrage
g = Grafcet(nb_etapes=3, etape_initiale=0) # un seul entier : fonctionne comme avant Réinitialisation (Règle 6)
g.reinitialiser() remet le GRAFCET dans sa situation initiale : désactive toutes les étapes, réactive les étapes initiales, remet les timers et compteurs à 0.
if arret_urgence:
g.reinitialiser() # retour à la situation initiale ⚠️ Divergence en OU
Si deux transitions partent de la même étape, leurs réceptivités doivent être exclusives (jamais vraies en même temps). Le moteur ne gère pas de priorité — si les deux sont vraies simultanément, les deux branches seront activées. C'est conforme à la norme (la responsabilité est au concepteur).
grafcet_variante.py — Moteur GRAFCET variante IEC 60848 .python avance/grafcet/ancien/grafcet_variante.py 359 lignes
GitHub
# =============================================================================
# grafcet_variante.py — Moteur GRAFCET pour MicroPython (variante IEC 60848)
# =============================================================================
# VARIANTE — version en cours de validation, voir grafcet.py et
# ascenseur_enim_led.py pour la version stable.
#
# Implémentation du standard IEC 60848 (GRAFCET / Sequential Function Chart)
# Auteur : Vincent — Fablab ESP32 Ardèche
#
# DIFFÉRENCES AVEC grafcet.py :
#
# 1. Validation automatique des transitions (Règle 2 IEC 60848)
# Le moteur vérifie lui-même que toutes les étapes sources d'une
# transition sont actives avant de la franchir. L'utilisateur n'écrit
# que la réceptivité pure (capteurs, timers, etc.) sans répéter
# g.etapes[i] dans calculer_transitions().
#
# 2. Franchissement simultané en deux passes (Règle 4 IEC 60848)
# Toutes les transitions simultanément franchissables sont franchies
# ensemble. Passe 1 : collecte. Passe 2 : application.
# Règle 5 : si une étape est à la fois désactivée et activée
# (activation/désactivation simultanées), elle reste active — son
# tempo et son compteur ne sont PAS remis à 0, aucun front n'est posé.
#
# 3. Documentation des modes de sortie (IEC 60848 §3.2)
# Voir section MODES DE SORTIE ci-dessous.
#
# PRINCIPE DU GRAFCET :
# Un GRAFCET est une machine à états séquentielle composée de :
# - étapes : états stables du système (actives ou inactives)
# - actions : ce que fait le système quand une étape est active
# - transitions : conditions logiques pour passer d'une étape à une autre
# - réceptivités : la condition associée à chaque transition
#
# MODES DE SORTIE (IEC 60848 §3.2) :
#
# Mode CONTINU (assignation) — la sortie suit l'état de l'étape :
# La sortie est vraie tant que l'étape est active, fausse sinon.
# C'est le mode par défaut, le plus simple et le plus sûr.
# Code type :
# led.value(g.etapes[i])
# Descendre = g.etapes[1]
#
# Mode MÉMORISÉ (affectation SET/RESET) — la sortie conserve sa valeur :
# La sortie prend une valeur à un instant précis et la conserve
# indépendamment de l'activité de l'étape. Utilise les fronts.
# 4 variantes :
# - À l'activation (rising) : if g.rising[i]: sortie = True
# - À la désactivation (falling) : if g.falling[i]: sortie = False
# - Sur événement d'entrée (fm) : if g.etapes[i] and g.fm[j]: sortie = True
# - Au franchissement : action déclenchée dans gerer_actions() sur un front
# Code type :
# if g.rising[1]: alarme = True # SET à l'entrée de l'étape 1
# if g.falling[2]: alarme = False # RESET à la fin de l'étape 2
#
# QUAND UTILISER QUEL MODE ?
# - Sortie liée à UNE SEULE étape → CONTINU
# La sortie suit l'étape : active = ON, inactive = OFF.
# Plus simple, plus lisible, et sécuritaire : si le programme plante,
# les sorties passent à 0 (état sûr).
# Ex: un moteur tourne pendant l'étape 1, s'arrête en sortant.
#
# - Sortie qui TRAVERSE PLUSIEURS étapes → MÉMORISÉ
# La sortie est activée dans une étape et désactivée dans une autre.
# Nécessaire quand le SET et le RESET sont dans des étapes différentes.
# Attention : si le programme plante entre SET et RESET, la sortie
# reste dans son dernier état (risque pour un moteur ou un actionneur).
# Ex: une alarme démarre à l'étape 1 et s'arrête à la fin de l'étape 3.
#
# RÈGLE : une variable de sortie ne doit être utilisée que dans UN SEUL
# mode (jamais continu ET mémorisé pour la même variable).
#
# LIMITATION : les évolutions fugaces ne sont pas gérées — une étape
# traversée en 1 seul cycle verra ses actions continues appliquées
# pendant 20 ms (la durée d'un cycle).
#
# DIVERGENCE EN OU (responsabilité du concepteur) :
# Si deux transitions partent de la même étape, leurs réceptivités
# DOIVENT être exclusives (jamais vraies en même temps). Le moteur
# ne gère pas de priorité — si les deux sont vraies, les deux
# branches seront activées (comportement non déterministe).
#
# RÉINITIALISATION (Règle 6 IEC 60848) :
# g.reinitialiser() remet le GRAFCET dans sa situation initiale.
# Utile pour arrêt d'urgence ou condition de forçage externe.
#
# ÉTAPES INITIALES MULTIPLES :
# etape_initiale peut être un entier ou une liste d'entiers.
# Ex: Grafcet(nb_etapes=6, etape_initiale=[0, 3])
#
# CYCLE D'EXÉCUTION NORMALISÉ (à respecter dans la boucle principale) :
# 1. franchir(T, trans) → franchissement des transitions validées
# (reset des fronts d'étape, puis pose des nouveaux)
# 2. tick() → mise à jour des timers
# 3. gerer_actions() → calcul des actions selon étapes actives
# (les fronts d'étape rising/falling sont lisibles ici)
# 4. affecter_sorties() → application des actions sur les sorties physiques
# 5. lire_entrees() → lecture des capteurs et boutons
# 6. detecter_fronts_entrees() → détection des fronts montants/descendants d'entrée
# 7. calculer_transitions() → évaluation des réceptivités UNIQUEMENT
# (le moteur vérifie la validation des étapes)
#
# NOTE : franchir() est en DÉBUT de cycle (pas en fin) pour que les fronts
# d'étape qu'il pose soient visibles par gerer_actions() du même cycle.
# =============================================================================
class Grafcet:
"""
Moteur d'exécution GRAFCET générique pour MicroPython.
Variante conforme IEC 60848 : validation automatique + franchissement simultané.
Usage sans fronts d'entrée :
g = Grafcet(nb_etapes=3)
while True:
g.franchir(T, transitions)
g.tick(20)
gerer_actions()
affecter_sorties()
lire_entrees()
calculer_transitions() # réceptivités pures, sans g.etapes[i]
synchro_ms(20)
Usage avec fronts d'entrée (nb_fronts = nombre de boutons/capteurs à surveiller) :
g = Grafcet(nb_etapes=3, nb_fronts=2)
while True:
g.franchir(T, transitions)
g.tick(20)
gerer_actions()
affecter_sorties()
lire_entrees() # → remplir g.entrees[i]
g.detecter_fronts_entrees() # → calcule g.fm[i] / g.fd[i]
calculer_transitions() # → utiliser g.fm[i] pour les fronts
synchro_ms(20)
Différence clé avec grafcet.py :
calculer_transitions() ne contient QUE les réceptivités (conditions
logiques). La validation des étapes sources est faite automatiquement
par franchir(). Exemple :
transitions[0] = Start and (g.tempo[0] > 200) # réceptivité pure
# PAS BESOIN de : g.etapes[0] and Start and ...
Modes de sortie dans gerer_actions() :
Continu (1 sortie = 1 étape) :
Descendre = g.etapes[1] # ON tant que étape 1 active
Mémorisé (1 sortie traverse plusieurs étapes) :
if g.rising[1]: alarme = True # SET à l'entrée étape 1
if g.falling[2]: alarme = False # RESET à la fin étape 2
"""
def __init__(self, nb_etapes, etape_initiale=0, nb_fronts=0):
"""
Initialise le GRAFCET.
:param nb_etapes: nombre total d'étapes (taille des tableaux internes)
:param etape_initiale: étape(s) active(s) au démarrage — un entier (défaut : 0)
ou une liste d'entiers pour plusieurs étapes initiales
ex: etape_initiale=[0, 3] pour activer étapes 0 et 3
:param nb_fronts: nombre d'entrées à surveiller pour les fronts (défaut : 0)
"""
self.nb_etapes = nb_etapes
# --- Tableau d'activation des étapes ---
# etapes[i] = True → l'étape i est active
# etapes[i] = False → l'étape i est inactive
# Plusieurs étapes peuvent être simultanément actives (divergence ET)
self.etapes = [False] * nb_etapes
# --- Timers par étape (en millisecondes) ---
# tempo[i] s'incrémente automatiquement tant que l'étape i est active
# tempo[i] est remis à 0 lors de la désactivation de l'étape
# Utilisation : transitions temporisées ex: tempo[0] > 500 (500 ms)
self.tempo = [0] * nb_etapes
# --- Compteurs par étape ---
# compt[i] peut être incrémenté manuellement dans gerer_actions()
# compt[i] est remis à 0 lors de la désactivation de l'étape
# Utilisation : compter des événements pendant qu'une étape est active
self.compt = [0] * nb_etapes
# --- Fronts montants d'étape (rising edge) ---
# rising[i] = True pendant UN SEUL cycle, au moment où l'étape i s'active
# Utilisation : actions mémorisées (SET) à l'entrée d'une étape
self.rising = [False] * nb_etapes
# --- Fronts descendants d'étape (falling edge) ---
# falling[i] = True pendant UN SEUL cycle, au moment où l'étape i se désactive
# Utilisation : actions mémorisées (RESET) à la sortie d'une étape
self.falling = [False] * nb_etapes
# Activation de l'étape initiale (Règle 1 IEC 60848)
# Mémorise la situation initiale pour reinitialiser()
if isinstance(etape_initiale, int):
self._init = [etape_initiale]
else:
self._init = list(etape_initiale)
for s in self._init:
self.etapes[s] = True
self.rising[s] = True # front montant au démarrage (actions mémorisées)
# --- Fronts d'entrée ---
# Détection des changements d'état des capteurs/boutons
# fm[i] = front montant d'entrée i (True pendant 1 cycle quand entrée passe False→True)
# fd[i] = front descendant d'entrée i (True pendant 1 cycle quand entrée passe True→False)
self.nb_fronts = nb_fronts
self.entrees = [False] * nb_fronts # état actuel (à remplir dans lire_entrees())
self.entrees_prec = [False] * nb_fronts # état du cycle précédent
self.fm = [False] * nb_fronts # fronts montants d'entrée
self.fd = [False] * nb_fronts # fronts descendants d'entrée
# -------------------------------------------------------------------------
def tick(self, dt_ms=20):
"""
Met à jour les timers internes. À appeler UNE FOIS par cycle de boucle,
APRÈS franchir() et AVANT gerer_actions().
- Incrémente tempo[i] pour chaque étape active
- Ne touche PAS aux fronts (rising/falling) — ceux-ci sont gérés par franchir()
:param dt_ms: durée du cycle en millisecondes — doit correspondre
à la valeur passée à synchro_ms() dans la boucle principale
"""
# Incrémente le timer de chaque étape actuellement active
for i in range(len(self.etapes)):
if self.etapes[i]:
self.tempo[i] += dt_ms # ex: après 10 cycles à 20ms → tempo[i] = 200
# -------------------------------------------------------------------------
def franchir(self, T, transitions):
"""
Franchit les transitions validées et met à jour le tableau des étapes.
Conforme aux règles IEC 60848 :
- Règle 2 : vérifie automatiquement que TOUTES les étapes sources
sont actives (validation). L'utilisateur ne fournit que la réceptivité.
- Règle 4 : toutes les transitions simultanément franchissables sont
franchies ensemble (deux passes : collecte puis application).
- Règle 5 : si une étape est simultanément désactivée et activée,
elle reste active (tempo et compt conservés, pas de front).
:param T: table de transitions — liste de tuples de la forme :
(indice_transition, (étapes_sources,), (étapes_cibles,))
Les étapes_sources servent à la fois pour :
- la validation (le moteur vérifie qu'elles sont actives)
- la désactivation (elles sont désactivées au franchissement)
Exemple pour un GRAFCET à 3 étapes en boucle :
T = [
(0, (0,), (1,)), # transition 0 : étape 0 → étape 1
(1, (1,), (2,)), # transition 1 : étape 1 → étape 2
(2, (2,), (0,)), # transition 2 : étape 2 → étape 0
]
Pour une convergence ET :
(4, (3, 5), (0,)), # transition 4 : étapes 3 ET 5 → étape 0
→ franchie seulement si étapes 3 ET 5 sont TOUTES actives
:param transitions: liste de booléens — transitions[i] = True si la
RÉCEPTIVITÉ de la transition i est satisfaite.
Ne PAS inclure g.etapes[i] — le moteur le vérifie.
"""
# Reset des fronts d'étape AVANT de poser les nouveaux
self.rising = [False] * len(self.etapes)
self.falling = [False] * len(self.etapes)
# --- PASSE 1 : collecte des étapes à désactiver et à activer ---
a_desactiver = set()
a_activer = set()
for t_id, sources, cibles in T:
# Règle 2 : la transition n'est franchissable que si
# TOUTES les étapes sources sont actives ET la réceptivité est vraie
toutes_actives = True
for s in sources:
if not self.etapes[s]:
toutes_actives = False
break
if toutes_actives and transitions[t_id]:
for s in sources:
a_desactiver.add(s)
for s in cibles:
a_activer.add(s)
# --- Règle 5 : activation/désactivation simultanées ---
# Si une étape apparaît dans les deux ensembles, elle reste active
# sans reset de tempo/compt et sans front
conflit = a_desactiver & a_activer
a_desactiver -= conflit
a_activer -= conflit
# --- PASSE 2 : application ---
# Désactivation des étapes sources
for s in a_desactiver:
self.falling[s] = True # front descendant pour ce cycle
self.etapes[s] = False
self.tempo[s] = 0
self.compt[s] = 0
# Activation des étapes cibles
for s in a_activer:
self.rising[s] = True # front montant pour ce cycle
self.etapes[s] = True
# -------------------------------------------------------------------------
def reinitialiser(self):
"""
Remet le GRAFCET dans sa situation initiale (Règle 6 IEC 60848).
- Désactive toutes les étapes (falling pour celles qui étaient actives)
- Réactive uniquement les étapes initiales (rising)
- Remet tous les timers et compteurs à 0
Usage : appeler quand un arrêt d'urgence ou une condition externe
impose de revenir à l'état de départ.
if arret_urgence:
g.reinitialiser()
"""
for i in range(self.nb_etapes):
if self.etapes[i] and i not in self._init:
self.falling[i] = True
self.etapes[i] = False
self.tempo[i] = 0
self.compt[i] = 0
for s in self._init:
self.etapes[s] = True
self.rising[s] = True
# -------------------------------------------------------------------------
def detecter_fronts_entrees(self):
"""
Détecte les fronts montants et descendants des entrées.
À appeler APRÈS lire_entrees() et AVANT calculer_transitions().
Compare self.entrees (état actuel, rempli par l'utilisateur dans
lire_entrees()) avec self.entrees_prec (état du cycle précédent).
Résultat :
- self.fm[i] = True si l'entrée i vient de passer de False à True
- self.fd[i] = True si l'entrée i vient de passer de True à False
"""
for i in range(self.nb_fronts):
self.fm[i] = self.entrees[i] and not self.entrees_prec[i]
self.fd[i] = not self.entrees[i] and self.entrees_prec[i]
self.entrees_prec[i] = self.entrees[i]
2 Ascenseur variante — réceptivités simplifiées
Même ascenseur que la version de base, adapté à grafcet_variante.py. La seule différence visible est dans calculer_transitions() — les g.etapes[i] and ont disparu :
def calculer_transitions():
# Réceptivités UNIQUEMENT — le moteur vérifie les étapes sources
transitions[0] = Start and (g.tempo[0] > 200) # T0 : départ
transitions[1] = Bas # T1 : fond atteint
transitions[2] = Haut # T2 : haut atteint Toutes les sorties sont en mode continu : la LED suit directement l'état de l'étape.
Fichiers nécessaires sur l'ESP32 : grafcet_variante.py, essential.py (sans OLED).
ascenseur_enim_led_variante.py — Ascenseur avec réceptivités pures .python avance/grafcet/ancien/ascenseur_enim_led_variante.py 243 lignes
GitHub
# =============================================================================
# ascenseur_enim_led_variante.py — Ascenseur avec grafcet_variante.py
# =============================================================================
# VARIANTE — version en cours de validation, voir grafcet.py et
# ascenseur_enim_led.py pour la version stable.
#
# Même ascenseur que ascenseur_enim_led.py, mais utilise grafcet_variante.py
# qui apporte deux changements conformes à la norme IEC 60848 :
#
# CE QUI CHANGE PAR RAPPORT À ascenseur_enim_led.py :
#
# 1. Import depuis grafcet_variante au lieu de grafcet
#
# 2. calculer_transitions() simplifié :
# Le moteur vérifie automatiquement que les étapes sources sont actives
# (Règle 2 IEC 60848). On n'écrit QUE les réceptivités.
#
# AVANT (grafcet.py) :
# transitions[0] = g.etapes[0] and Start and (g.tempo[0] > 200)
# transitions[1] = g.etapes[1] and Bas
# transitions[2] = g.etapes[2] and Haut
#
# APRÈS (grafcet_variante.py) :
# transitions[0] = Start and (g.tempo[0] > 200)
# transitions[1] = Bas
# transitions[2] = Haut
#
# 3. Franchissement simultané (Règle 4) : toutes les transitions
# franchissables dans le même cycle sont franchies ensemble.
# Dans cet exemple simple (séquence linéaire), ça ne change rien.
# La différence est visible avec des divergences EN OU.
#
# MODES DE SORTIE (les deux modes dans le même programme) :
#
# Mode CONTINU (assignation) — sortie liée à UNE SEULE étape :
# La sortie suit l'étape : active = ON, inactive = OFF.
# Plus simple et sécuritaire (si le programme plante, tout s'éteint).
# led_bleue = g.etapes[0] (allumée tant que étape 0 active)
# led_verte = Descendre (allumée tant que étape 1 active)
# led_jaune = Monter (allumée tant que étape 2 active)
# moteurs = idem (tournent tant que l'étape est active)
#
# Mode MÉMORISÉ (SET/RESET) — sortie qui TRAVERSE PLUSIEURS étapes :
# La sortie est activée dans une étape et désactivée dans une autre.
# Ici, led_rouge clignote de l'étape 1 (descente) à la fin de l'étape 2
# (montée) — elle couvre 2 étapes, impossible en mode continu.
# SET : g.rising[1] → clignoter = True (entrée étape Descente)
# RESET: g.falling[2] → clignoter = False (fin étape Montée)
# ⚠ Attention : si le programme plante entre SET et RESET, la sortie
# reste dans son dernier état.
#
# CORRESPONDANCE CARTE ENIM ↔ ASCENSEUR :
#
# Entrées :
# bpA (Pin 25) → bouton Start
# bpB (Pin 34) → fin de course HAUT
# bpC (Pin 39) → fin de course BAS
#
# Sorties :
# led_bleue (Pin 2) → témoin étape 0 (repos) [CONTINU]
# led_verte (Pin 18) → commande Descente [CONTINU]
# led_jaune (Pin 19) → commande Montée [CONTINU]
# led_rouge (Pin 23) → alarme clignotante en descente [MÉMORISÉ]
# np (Pin 26) → indicateur de niveau (barre NeoPixel 8 LEDs)
# Pin 12 → sortie réelle Descente (driver moteur / relais)
# Pin 13 → sortie réelle Montée (driver moteur / relais)
#
# GRAFCET (3 étapes) :
#
# ┌──────────────────────────────────────┐
# │ ÉTAPE 0 — Repos │ led_bleue allumée
# └──────────────────┬───────────────────┘
# │ T0 : bpA pressé ET tempo[0] > 200 ms
# ┌──────────────────▼───────────────────┐
# │ ÉTAPE 1 — Descente │ led_verte allumée
# └──────────────────┬───────────────────┘
# │ T1 : bpC actif OU niveau simulé ≤ -99
# ┌──────────────────▼───────────────────┐
# │ ÉTAPE 2 — Montée │ led_jaune allumée
# └──────────────────┬───────────────────┘
# │ T2 : bpB actif OU niveau simulé ≥ -1
# └────────────────────────────► ÉTAPE 0
#
# Fichiers nécessaires sur l'ESP32 :
# grafcet_variante.py (moteur GRAFCET variante IEC 60848)
# essential.py (déclarations carte ENIM — sans OLED)
# =============================================================================
from machine import Pin
from time import ticks_ms
from grafcet_variante import Grafcet
from essential import (
synchro_ms, # synchronisation cycle 20 ms
bpA, # bouton Start (Pin 25)
bpB, # fin de course HAUT (Pin 34)
bpC, # fin de course BAS (Pin 39)
led_bleue, # témoin repos (Pin 2)
led_verte, # commande Descente (Pin 18)
led_jaune, # commande Montée (Pin 19)
led_rouge, # alarme clignotante (Pin 23)
np, # NeoPixel 8 LEDs (Pin 26)
)
# Broches libres pour un vrai moteur (optionnel)
sortie_descente = Pin(12, Pin.OUT) # driver moteur / relais Descente
sortie_montee = Pin(13, Pin.OUT) # driver moteur / relais Montée
# Sécurité : sorties moteur à 0 au démarrage
sortie_descente.value(0)
sortie_montee.value(0)
# NeoPixel : position initiale de la cabine (en haut = LED 0)
for led in range(8):
np[led] = (0, 0, 0)
np[0] = (0, 50, 0) # cabine en position haute
np.write()
# =============================================================================
# INITIALISATION DU MOTEUR GRAFCET
# =============================================================================
g = Grafcet(nb_etapes=3, etape_initiale=0)
T = [
(0, (0,), (1,)), # T0 : Repos → Descente
(1, (1,), (2,)), # T1 : Descente → Montée
(2, (2,), (0,)), # T2 : Montée → Repos
]
# =============================================================================
# VARIABLES
# =============================================================================
Descendre = False
Monter = False
Start = False
Haut = False
Bas = False
clignoter = False # mode MÉMORISÉ : led_rouge clignote pendant la descente
niveau = 0 # position simulée : 0 = haut, -100 = bas
vitesse = 1 # déplacement par cycle
x_ancien = 0 # dernière position NeoPixel affichée
transitions = [False] * len(T)
# =============================================================================
# SIMULATION DU NIVEAU — NeoPixel uniquement
# =============================================================================
def ascenseur(inc):
"""
Déplace la cabine simulée et met à jour le bandeau NeoPixel.
:param inc: -vitesse = descente, +vitesse = montée
"""
global niveau, x_ancien
# Déplacement avec butées
niveau = niveau + inc
if niveau < -100: niveau = -100 # plancher
if niveau > 0: niveau = 0 # plafond
# Conversion en position NeoPixel (0 = haut, 7 = bas)
x = abs(int(-niveau / 12.6))
# Rafraîchissement uniquement si la position a changé
if x != x_ancien:
for led in range(0, x): np[led] = (0, 30, 0) # sous la cabine : vert
for led in range(x, 8): np[led] = (0, 0, 0) # au-dessus : éteint
np[x] = (0, 50, 0) # position cabine : vert vif
np.write()
x_ancien = x
# =============================================================================
# CYCLE GRAFCET — mode continu + mémorisé
# =============================================================================
def gerer_actions():
global Descendre, Monter, clignoter
# --- Mode CONTINU (assignation) ---
if g.etapes[0]: Descendre = False ; Monter = False # Repos
if g.etapes[1]: Descendre = True ; Monter = False # Descente
if g.etapes[2]: Descendre = False ; Monter = True # Montée
# --- Mode MÉMORISÉ (SET/RESET) ---
# clignoter est SET à l'entrée de l'étape Descente,
# et RESET à l'entrée de l'étape Montée.
# La variable conserve sa valeur entre les deux fronts.
if g.rising[1]: clignoter = True # SET : début descente → alarme ON
if g.falling[2]: clignoter = False # RESET : fin de montée → alarme OFF
def affecter_sorties():
# Sorties continues
led_bleue.value(g.etapes[0]) # témoin repos
led_verte.value(Descendre) # LED descente
led_jaune.value(Monter) # LED montée
sortie_descente.value(Descendre) # sortie moteur Descente
sortie_montee.value(Monter) # sortie moteur Montée
if Descendre: ascenseur(-vitesse) # simulation descente
if Monter: ascenseur(+vitesse) # simulation montée
# Sortie mémorisée : LED rouge clignote à 2 Hz (250 ms ON / 250 ms OFF)
if clignoter:
led_rouge.value(ticks_ms() % 500 < 250)
else:
led_rouge.value(0)
def lire_entrees():
global Start, Haut, Bas
Start = bpA.value()
Bas = bpC.value() or (niveau <= -99) # capteur OU butée simulée
Haut = bpB.value() or (niveau >= -1) # capteur OU butée simulée
def calculer_transitions():
# Réceptivités UNIQUEMENT — le moteur vérifie les étapes sources
# (plus besoin de g.etapes[i] and ...)
transitions[0] = Start and (g.tempo[0] > 200) # T0 : départ
transitions[1] = Bas # T1 : fond atteint
transitions[2] = Haut # T2 : haut atteint
# =============================================================================
# BOUCLE PRINCIPALE
# =============================================================================
while True:
g.franchir(T, transitions)
g.tick(20)
gerer_actions()
affecter_sorties()
lire_entrees()
calculer_transitions()
synchro_ms(20)
🎯 Version complète — référence pédagogique
Un seul moteur + un seul exemple qui démontrent TOUTES les fonctionnalités. C'est la base pour tous les projets.
1 Moteur GRAFCET complet
Version de référence intégrant toutes les fonctionnalités en un seul fichier :
- Validation automatique des étapes sources (Règle 2)
- Franchissement simultané en deux passes (Règle 4 + Règle 5)
- Fronts d'étape :
rising[i]/falling[i] - Fronts d'entrée :
fm[i]/fd[i](optionnel vianb_fronts) - Timers
tempo[i]et compteurscompt[i] - Étapes initiales multiples
reinitialiser()— arrêt d'urgence (Règle 6)
grafcet_complet.py — Moteur GRAFCET de référence .python avance/grafcet/grafcet_complet.py 245 lignes
GitHub
# =============================================================================
# grafcet_complet.py — Moteur GRAFCET complet pour MicroPython
# =============================================================================
# Implémentation conforme au standard IEC 60848 (GRAFCET / SFC)
# Auteur : Vincent — Fablab ESP32 Ardèche
#
# Ce fichier est la VERSION DE RÉFÉRENCE du moteur GRAFCET.
# Il intègre toutes les fonctionnalités dans un seul fichier :
#
# FONCTIONNALITÉS :
# - Étapes, timers (tempo), compteurs (compt)
# - Fronts d'étape : rising (activation) et falling (désactivation)
# - Fronts d'entrée : fm (front montant) et fd (front descendant)
# - Validation automatique des transitions (Règle 2 IEC 60848)
# - Franchissement simultané en deux passes (Règle 4)
# - Gestion des conflits activation/désactivation (Règle 5)
# - Étapes initiales multiples (Règle 1)
# - Réinitialisation (Règle 6) — arrêt d'urgence
#
# PRINCIPE DU GRAFCET :
# Un GRAFCET est une machine à états séquentielle composée de :
# - étapes : états stables du système (actives ou inactives)
# - actions : ce que fait le système quand une étape est active
# - transitions : conditions logiques pour passer d'une étape à une autre
# - réceptivités : la condition associée à chaque transition
#
# MODES DE SORTIE (IEC 60848 §3.2) :
#
# Mode CONTINU (assignation) — sortie liée à UNE SEULE étape :
# La sortie suit l'étape : active = ON, inactive = OFF.
# Plus simple et sécuritaire (si le programme plante, tout s'éteint).
# Code type :
# led.value(g.etapes[i])
# Descendre = g.etapes[1]
#
# Mode MÉMORISÉ (affectation SET/RESET) — sortie qui TRAVERSE PLUSIEURS étapes :
# La sortie prend une valeur à un instant précis et la conserve.
# Nécessaire quand le SET et le RESET sont dans des étapes différentes.
# Attention : si le programme plante entre SET et RESET, la sortie
# reste dans son dernier état.
# 4 variantes :
# - À l'activation : if g.rising[i]: sortie = True
# - À la désactivation : if g.falling[i]: sortie = False
# - Sur front d'entrée : if g.etapes[i] and g.fm[j]: sortie = True
# - Au franchissement : action déclenchée sur un front
# Code type :
# if g.rising[1]: alarme = True # SET à l'entrée de l'étape 1
# if g.falling[2]: alarme = False # RESET à la fin de l'étape 2
#
# RÈGLE : une variable de sortie ne doit être utilisée que dans UN SEUL
# mode (jamais continu ET mémorisé pour la même variable).
#
# DIVERGENCE EN OU (responsabilité du concepteur) :
# Si deux transitions partent de la même étape, leurs réceptivités
# DOIVENT être exclusives (jamais vraies en même temps). Le moteur
# ne gère pas de priorité — si les deux sont vraies, les deux
# branches seront activées (comportement non déterministe).
#
# CYCLE D'EXÉCUTION NORMALISÉ (à respecter dans la boucle principale) :
# 1. franchir(T, trans) → franchissement des transitions validées
# 2. tick() → mise à jour des timers
# 3. gerer_actions() → calcul des actions (fronts lisibles ici)
# 4. affecter_sorties() → application sur les sorties physiques
# 5. lire_entrees() → lecture des capteurs et boutons
# 6. detecter_fronts_entrees() → détection des fronts d'entrée (fm/fd)
# 7. calculer_transitions() → réceptivités UNIQUEMENT
#
# NOTE : franchir() est en DÉBUT de cycle pour que les fronts d'étape
# soient visibles par gerer_actions() du même cycle.
# =============================================================================
class Grafcet:
"""
Moteur d'exécution GRAFCET complet pour MicroPython.
Conforme IEC 60848 : validation automatique, franchissement simultané,
fronts d'étape et d'entrée, compteurs, réinitialisation.
Usage :
g = Grafcet(nb_etapes=3, nb_fronts=1)
while True:
g.franchir(T, transitions)
g.tick(20)
gerer_actions()
affecter_sorties()
lire_entrees() # → remplir g.entrees[i]
g.detecter_fronts_entrees() # → calcule g.fm[i] / g.fd[i]
calculer_transitions() # → réceptivités pures
synchro_ms(20)
Attributs principaux :
g.etapes[i] — True si l'étape i est active
g.tempo[i] — durée en ms depuis l'activation de l'étape i
g.compt[i] — compteur libre, incrémentable dans gerer_actions()
g.compt_final[i] — dernière valeur de compt[i] avant reset (lisible sur falling)
g.rising[i] — True pendant 1 cycle à l'activation de l'étape i
g.falling[i] — True pendant 1 cycle à la désactivation de l'étape i
g.entrees[i] — état brut de l'entrée i (à remplir dans lire_entrees)
g.fm[i] — front montant de l'entrée i (1 cycle)
g.fd[i] — front descendant de l'entrée i (1 cycle)
Modes de sortie dans gerer_actions() :
Continu (1 sortie = 1 étape) :
Descendre = g.etapes[1]
Mémorisé (1 sortie traverse plusieurs étapes) :
if g.rising[1]: alarme = True # SET
if g.falling[2]: alarme = False # RESET
"""
def __init__(self, nb_etapes, etape_initiale=0, nb_fronts=0):
"""
Initialise le GRAFCET.
:param nb_etapes: nombre total d'étapes
:param etape_initiale: étape(s) active(s) au démarrage — int ou liste
:param nb_fronts: nombre d'entrées à surveiller pour les fronts (défaut : 0)
"""
self.nb_etapes = nb_etapes
self.etapes = [False] * nb_etapes # activation des étapes
self.tempo = [0] * nb_etapes # timers (ms)
self.compt = [0] * nb_etapes # compteurs libres
self.compt_final = [0] * nb_etapes # dernière valeur de compt avant reset
self.rising = [False] * nb_etapes # fronts montants d'étape
self.falling = [False] * nb_etapes # fronts descendants d'étape
# Étapes initiales (Règle 1) — mémorisées pour reinitialiser()
if isinstance(etape_initiale, int):
self._init = [etape_initiale]
else:
self._init = list(etape_initiale)
for s in self._init:
self.etapes[s] = True
self.rising[s] = True # front montant au démarrage (actions mémorisées)
# Flag pour que franchir() préserve ce rising au premier appel
self._skip_reset = True
# Fronts d'entrée (optionnel)
self.nb_fronts = nb_fronts
self.entrees = [False] * nb_fronts
self.entrees_prec = [False] * nb_fronts
self.fm = [False] * nb_fronts
self.fd = [False] * nb_fronts
# -------------------------------------------------------------------------
def tick(self, dt_ms=20):
"""Incrémente les timers des étapes actives."""
for i in range(self.nb_etapes):
if self.etapes[i]:
self.tempo[i] += dt_ms
# -------------------------------------------------------------------------
def franchir(self, T, transitions):
"""
Franchit les transitions validées (Règles 2, 4, 5 IEC 60848).
Le moteur vérifie automatiquement que toutes les étapes sources sont
actives. L'utilisateur ne fournit que les réceptivités.
Deux passes : collecte puis application. Si une étape est à la fois
désactivée et activée (Règle 5), elle reste active sans reset.
:param T: table de transitions (indice, sources, cibles)
:param transitions: liste de booléens (réceptivités pures)
"""
if self._skip_reset:
self._skip_reset = False
else:
self.rising = [False] * self.nb_etapes
self.falling = [False] * self.nb_etapes
# Passe 1 : collecte
a_desactiver = set()
a_activer = set()
for t_id, sources, cibles in T:
toutes_actives = True
for s in sources:
if not self.etapes[s]:
toutes_actives = False
break
if toutes_actives and transitions[t_id]:
for s in sources:
a_desactiver.add(s)
for s in cibles:
a_activer.add(s)
# Règle 5 : conflits
conflit = a_desactiver & a_activer
a_desactiver -= conflit
a_activer -= conflit
# Passe 2 : application
for s in a_desactiver:
self.falling[s] = True
self.compt_final[s] = self.compt[s] # sauvegarde avant reset
self.etapes[s] = False
self.tempo[s] = 0
self.compt[s] = 0
for s in a_activer:
self.rising[s] = True
self.etapes[s] = True
# -------------------------------------------------------------------------
def reinitialiser(self):
"""
Remet le GRAFCET dans sa situation initiale (Règle 6 IEC 60848).
Désactive toutes les étapes, réactive les étapes initiales,
remet tous les timers et compteurs à 0.
"""
for i in range(self.nb_etapes):
if self.etapes[i] and i not in self._init:
self.falling[i] = True
self.compt_final[i] = self.compt[i]
self.etapes[i] = False
self.tempo[i] = 0
self.compt[i] = 0
for s in self._init:
self.etapes[s] = True
self.rising[s] = True
self._skip_reset = True # préserver les fronts pour gerer_actions()
# -------------------------------------------------------------------------
def detecter_fronts_entrees(self):
"""
Détecte les fronts montants (fm) et descendants (fd) des entrées.
Compare entrees (état actuel) avec entrees_prec (cycle précédent).
"""
for i in range(self.nb_fronts):
self.fm[i] = self.entrees[i] and not self.entrees_prec[i]
self.fd[i] = not self.entrees[i] and self.entrees_prec[i]
self.entrees_prec[i] = self.entrees[i]
2 Ascenseur complet — toutes les fonctionnalités en action
Le même ascenseur 3 étapes, mais qui démontre chaque fonctionnalité du moteur :
| Fonctionnalité | Comment c'est utilisé |
|---|---|
| Mode continu | led_bleue, led_verte, led_jaune, moteurs — suivent l'étape |
| Mode mémorisé | led_rouge clignote 2 Hz — SET sur rising[1], RESET sur falling[2] |
Front d'entrée fm | bpA détecté en front montant — un appui = un cycle |
Front d'étape rising | SET clignoter + incrémente le compteur de cycles |
Front d'étape falling | RESET clignoter à la fin de la montée |
Temporisation tempo | 500 ms d'anti-rebond au repos avant nouveau départ |
Compteur compt | Compte les aller-retours — après 5 → "MAINTENANCE" |
| Réinitialisation | bpD = arrêt d'urgence → g.reinitialiser() |
| Validation auto | calculer_transitions() ne contient que les réceptivités |
Fichiers nécessaires sur l'ESP32 : grafcet_complet.py, essential.py.
La console Thonny affiche les événements : numéro de cycle, maintenance, arrêt d'urgence.
ascenseur_complet.py — Exemple de référence complet .python avance/grafcet/ascenseur_complet.py 303 lignes
GitHub
# =============================================================================
# ascenseur_complet.py — Exemple de référence GRAFCET complet
# =============================================================================
# Ascenseur simulé sur carte ENIM utilisant TOUTES les fonctionnalités
# du moteur grafcet_complet.py. Cet exemple sert de référence pédagogique
# pour le cours avancé GRAFCET du Fablab.
#
# FONCTIONNALITÉS DÉMONTRÉES :
#
# ┌─────────────────────────────────────────────────────────────────────┐
# │ Fonctionnalité │ Où dans le code │
# ├─────────────────────────────┼──────────────────────────────────────┤
# │ Mode CONTINU │ led_bleue, led_verte, led_jaune, │
# │ (1 sortie = 1 étape) │ moteurs descente/montée │
# ├─────────────────────────────┼──────────────────────────────────────┤
# │ Mode MÉMORISÉ (SET/RESET) │ led_rouge clignote 2 Hz │
# │ (sortie traverse 2 étapes) │ SET=rising[1], RESET=falling[2] │
# ├─────────────────────────────┼──────────────────────────────────────┤
# │ Front montant d'entrée (fm) │ bpA=fm[0] (start), bpD=fm[1] │
# │ │ (arrêt d'urgence en front) │
# ├─────────────────────────────┼──────────────────────────────────────┤
# │ Front montant d'étape │ g.rising[0] → bip buzzer 200 ms │
# │ (rising) │ g.rising[1] → SET clignoter │
# ├─────────────────────────────┼──────────────────────────────────────┤
# │ Front descendant d'étape │ g.falling[2] → RESET clignoter │
# │ (falling) │ │
# ├─────────────────────────────┼──────────────────────────────────────┤
# │ Temporisation (tempo) │ tempo[0] > 500 : anti-rebond au │
# │ │ repos (500 ms avant nouveau départ) │
# ├─────────────────────────────┼──────────────────────────────────────┤
# │ Compteur (nb_cycles) │ variable Python comptant les │
# │ │ aller-retours. Après 5 → bloqué. │
# ├─────────────────────────────┼──────────────────────────────────────┤
# │ Compteur d'étape (compt) │ g.compt[2] compte les appuis bpA │
# │ │ pendant la montée (remis à 0 auto) │
# ├─────────────────────────────┼──────────────────────────────────────┤
# │ Réinitialisation (Règle 6) │ bpD = arrêt d'urgence → │
# │ │ g.reinitialiser() │
# ├─────────────────────────────┼──────────────────────────────────────┤
# │ Validation auto (Règle 2) │ calculer_transitions() ne contient │
# │ │ que les réceptivités pures │
# └─────────────────────────────┴──────────────────────────────────────┘
#
# CORRESPONDANCE CARTE ENIM ↔ ASCENSEUR :
#
# Entrées :
# bpA (Pin 25) → bouton Start (front montant détecté)
# bpB (Pin 34) → fin de course HAUT
# bpC (Pin 39) → fin de course BAS
# bpD (Pin 36) → ARRÊT D'URGENCE → reinitialiser()
#
# Sorties :
# led_bleue (Pin 2) → témoin repos [CONTINU]
# led_verte (Pin 18) → commande Descente [CONTINU]
# led_jaune (Pin 19) → commande Montée [CONTINU]
# led_rouge (Pin 23) → alarme clignotante 2 Hz [MÉMORISÉ]
# buzzer (Pin 5) → bip "système prêt" 200 ms [MÉMORISÉ]
# np (Pin 26) → indicateur de niveau NeoPixel
# Pin 12 → sortie réelle Descente
# Pin 13 → sortie réelle Montée
#
# GRAFCET (3 étapes) :
#
# ┌──────────────────────────────────────┐
# │ ÉTAPE 0 — Repos │ led_bleue ON
# │ bip buzzer 200 ms (rising[0]) │ "système prêt"
# │ nb_cycles++ à chaque retour (rising)│ affiche nb cycles
# └──────────────────┬───────────────────┘
# │ T0 : fm[0] (front montant bpA)
# │ ET tempo[0] > 500 ms (anti-rebond)
# │ ET pas en maintenance (< 5 cycles)
# ┌──────────────────▼───────────────────┐
# │ ÉTAPE 1 — Descente │ led_verte ON
# │ SET clignoter (rising[1]) │ led_rouge clignote
# │ │
# └──────────────────┬───────────────────┘
# │ T1 : bpC actif OU niveau simulé ≤ -99
# ┌──────────────────▼───────────────────┐
# │ ÉTAPE 2 — Montée │ led_jaune ON
# │ compt[2]++ si appui bpA (fm[0]) │ (compteur d'étape)
# │ RESET clignoter (falling[2]) │ led_rouge clignote
# └──────────────────┬───────────────────┘ puis s'arrête
# │ T2 : bpB actif OU niveau simulé ≥ -1
# └────────────────────────────► ÉTAPE 0
#
# À tout moment : fm[1] (front montant bpD) → reinitialiser()
#
# Fichiers nécessaires sur l'ESP32 :
# grafcet_complet.py (moteur GRAFCET)
# essential.py (déclarations carte ENIM — sans OLED)
# =============================================================================
from machine import Pin
from time import ticks_ms
from grafcet_complet import Grafcet
from essential import (
synchro_ms, # synchronisation cycle 20 ms
bpA, # bouton Start (Pin 25)
bpB, # fin de course HAUT (Pin 34)
bpC, # fin de course BAS (Pin 39)
bpD, # ARRÊT D'URGENCE (Pin 36)
led_bleue, # témoin repos (Pin 2)
led_verte, # commande Descente (Pin 18)
led_jaune, # commande Montée (Pin 19)
led_rouge, # alarme clignotante (Pin 23)
buzzer, # bip "système prêt" (Pin 5)
np, # NeoPixel 8 LEDs (Pin 26)
)
# Broches libres pour un vrai moteur (optionnel)
sortie_descente = Pin(12, Pin.OUT)
sortie_montee = Pin(13, Pin.OUT)
# Sécurité : sorties moteur à 0 au démarrage
sortie_descente.value(0)
sortie_montee.value(0)
# NeoPixel : position initiale de la cabine (en haut = LED 0)
for led in range(8):
np[led] = (0, 0, 0)
np[0] = (0, 50, 0)
np.write()
# =============================================================================
# INITIALISATION DU MOTEUR GRAFCET
# =============================================================================
# nb_fronts=2 : front montant de bpA (entrée 0) et bpD (entrée 1)
g = Grafcet(nb_etapes=3, nb_fronts=2)
T = [
(0, (0,), (1,)), # T0 : Repos → Descente
(1, (1,), (2,)), # T1 : Descente → Montée
(2, (2,), (0,)), # T2 : Montée → Repos
]
# =============================================================================
# VARIABLES
# =============================================================================
Descendre = False
Monter = False
Bas = False
Haut = False
clignoter = False # mode MÉMORISÉ : led_rouge clignote pendant descente+montée
nb_cycles = -1 # -1 = pas encore de vrai cycle (le rising[0] du démarrage ne compte pas)
maintenance = False # True après 5 cycles → bloque le départ
niveau = 0 # position simulée : 0 = haut, -100 = bas
vitesse = 1 # déplacement par cycle
x_ancien = 0 # dernière position NeoPixel affichée
transitions = [False] * len(T)
# =============================================================================
# SIMULATION DU NIVEAU — NeoPixel uniquement
# =============================================================================
def ascenseur(inc):
global niveau, x_ancien
niveau = niveau + inc
if niveau < -100: niveau = -100
if niveau > 0: niveau = 0
x = abs(int(-niveau / 12.6))
if x != x_ancien:
for led in range(0, x): np[led] = (0, 30, 0)
for led in range(x, 8): np[led] = (0, 0, 0)
np[x] = (0, 50, 0)
np.write()
x_ancien = x
# =============================================================================
# CYCLE GRAFCET
# =============================================================================
def gerer_actions():
global Descendre, Monter, clignoter, nb_cycles, maintenance
# --- Mode CONTINU (1 sortie = 1 étape) ---
if g.etapes[0]: Descendre = False ; Monter = False
if g.etapes[1]: Descendre = True ; Monter = False
if g.etapes[2]: Descendre = False ; Monter = True
# --- Mode MÉMORISÉ : bip "système prêt" (rising[0] + tempo[0]) ---
# Action à l'activation de l'étape 0 : bip buzzer 200 ms
# Se déclenche au démarrage ET à chaque retour au repos
if g.rising[0]:
buzzer.init(freq=1000, duty=50)
if g.etapes[0] and g.tempo[0] > 200:
buzzer.deinit()
# --- Mode MÉMORISÉ : LED rouge clignotante (SET/RESET — traverse 2 étapes) ---
# La LED rouge clignote de l'étape 1 (descente) à la fin de l'étape 2 (montée)
if g.rising[1]: clignoter = True # SET : début descente → alarme ON
if g.falling[2]: clignoter = False # RESET : fin montée → alarme OFF
# --- Compteur d'étape (g.compt) ---
# g.compt[2] compte les appuis sur bpA PENDANT la montée (étape 2)
# On ne compte pas pendant la descente car le fm[0] du démarrage
# est encore visible quand l'étape 1 s'active (faux comptage).
# compt est remis à 0 automatiquement quand l'étape est désactivée.
# Pour lire la valeur finale, utiliser compt_final[i] sur falling[i].
if g.etapes[2] and g.fm[0]:
g.compt[2] += 1
if g.falling[2]:
print(" Appuis bpA pendant montée :", g.compt_final[2])
# --- Compteur d'aller-retours ---
# nb_cycles s'incrémente à chaque retour au repos (front montant étape 0)
# Initialisé à -1 : le rising[0] du démarrage fait passer à 0 (ne compte pas).
# Les vrais cycles commencent à 1.
if g.rising[0]:
nb_cycles += 1
if nb_cycles > 0:
print("Cycle", nb_cycles, "terminé")
if nb_cycles >= 5:
maintenance = True
print(">>> MAINTENANCE : 5 cycles atteints, redémarrage bloqué")
def affecter_sorties():
# Sorties continues
led_bleue.value(g.etapes[0])
led_verte.value(Descendre)
led_jaune.value(Monter)
sortie_descente.value(Descendre)
sortie_montee.value(Monter)
if Descendre: ascenseur(-vitesse)
if Monter: ascenseur(+vitesse)
# Sortie mémorisée : LED rouge clignote à 2 Hz (250 ms ON / 250 ms OFF)
if clignoter:
led_rouge.value(ticks_ms() % 500 < 250)
else:
led_rouge.value(0)
def lire_entrees():
global Bas, Haut
# Fronts d'entrée : bpA (entrée 0) et bpD (entrée 1)
# On écrit l'état brut, detecter_fronts_entrees() calcule g.fm[0] et g.fm[1]
g.entrees[0] = bpA.value()
g.entrees[1] = bpD.value()
# Entrées classiques (niveau)
Bas = bpC.value() or (niveau <= -99)
Haut = bpB.value() or (niveau >= -1)
def calculer_transitions():
# Réceptivités UNIQUEMENT — le moteur vérifie les étapes sources (Règle 2)
#
# T0 : front montant de bpA (g.fm[0]) — un appui = un cycle
# ET tempo[0] > 500 ms — anti-rebond, empêche le redémarrage trop rapide
# ET pas en maintenance — bloque après 5 cycles
transitions[0] = g.fm[0] and (g.tempo[0] > 500) and not maintenance
transitions[1] = Bas
transitions[2] = Haut
# =============================================================================
# BOUCLE PRINCIPALE — cycle GRAFCET normalisé (7 phases)
# =============================================================================
print("Ascenseur GRAFCET complet — bpA=Start, bpD=Arrêt d'urgence")
print("Appuyer sur bpA pour démarrer un cycle")
while True:
g.franchir(T, transitions) # 1. évolution + fronts d'étape
# Arrêt d'urgence : front montant de bpD (g.fm[1])
# Placé APRÈS franchir() pour que les fronts posés par reinitialiser()
# soient visibles par gerer_actions() (ils survivent jusqu'au prochain franchir)
if g.fm[1]:
g.reinitialiser()
clignoter = False # éteindre l'alarme
nb_cycles = -1 # -1 car reinit pose rising[0]
maintenance = False # débloquer
led_rouge.value(0) # éteindre la LED rouge immédiatement
buzzer.deinit() # couper le buzzer
sortie_descente.value(0) # couper le moteur descente
sortie_montee.value(0) # couper le moteur montée
# NeoPixel et niveau gardent leur position — l'AU fige le système
print("!!! ARRÊT D'URGENCE — réinitialisation !!!")
g.tick(20) # 2. timers
gerer_actions() # 3. actions (fronts lisibles ici)
affecter_sorties() # 4. sorties physiques
lire_entrees() # 5. capteurs/boutons → g.entrees[i]
g.detecter_fronts_entrees() # 6. fronts d'entrée → g.fm[i] / g.fd[i]
calculer_transitions() # 7. réceptivités pures
synchro_ms(20)
Adapter à ton propre système
Garde grafcet.py intact. Crée ton propre fichier avec ta table T et tes 4 fonctions. Divergence ET : (0, (0,), (1, 2)) active les étapes 1 et 2 simultanément.
Pour aller plus loin : voir le module NeoPixel progressif qui ajoute un affichage visuel fluide au même ascenseur.