GRAFCET & Programmation séquentielle
Le standard industriel français pour commander des systèmes séquentiels — implémenté en MicroPython sur ESP32. Ascenseurs, escape games, machines automatisées : tout ce qui doit faire des choses dans un ordre précis.
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. 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 toutes les étapes sources sont actives, la transition est franchie.
Les règles IEC 60848
| Règle | Principe | Dans le moteur |
|---|---|---|
| 1 | Situation initiale | etape_initiale — int ou liste |
| 2 | Validation = sources actives + réceptivité | franchir() vérifie automatiquement |
| 3 | Évolution : désactivation sources, activation cibles | Passe 2 de franchir() |
| 4 | Franchissement simultané | Deux passes : collecte puis application |
| 5 | Activation/désactivation simultanées | L'étape reste active (tempo, compt conservés) |
| 6 | Réinitialisation | g.reinitialiser() |
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 :
| # | Phase | Ce qu'elle fait |
|---|---|---|
| 1 | franchir(T, trans) | Évolution : reset des fronts d'étape, validation auto des sources, franchissement simultané |
| 2 | tick() | Incrémente les timers des étapes actives |
| 3 | gerer_actions() | Calcule les actions — les fronts rising/falling sont lisibles ici |
| 4 | affecter_sorties() | Applique sur le matériel (LEDs, moteurs...) |
| 5 | lire_entrees() | Lit les capteurs et boutons → g.entrees[i] |
| 6 | detecter_fronts_entrees() | Détecte les changements d'état → g.fm[i] / g.fd[i] |
| 7 | calculer_transitions() | Évalue les réceptivités (pas de g.etapes[i] — le moteur vérifie) |
Pourquoi cet ordre ?
franchir() est en début de cycle : les fronts d'étape qu'il pose sont visibles par gerer_actions().
Les sorties sont affectées avant la lecture des entrées pour éviter qu'une sortie influence une entrée dans le même cycle.
Les modes de sortie (IEC 60848 §3.2)
Mode CONTINU (assignation)
Quand : la sortie est liée à une seule étape. Active = ON, inactive = OFF.
Sécurité : si le programme plante, tout s'éteint.
Descendre = g.etapes[1]
led.value(g.etapes[0]) 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 active.
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é. Ne jamais mélanger les deux modes pour la même variable.
Les attributs du moteur
| Attribut | Type | Rôle |
|---|---|---|
| g.etapes[i] | bool | True si l'étape i est active |
| g.tempo[i] | int | Durée en ms depuis l'activation (reset auto à la désactivation) |
| g.compt[i] | int | Compteur libre dans l'étape (reset auto à la désactivation) |
| g.compt_final[i] | int | Dernière valeur de compt avant reset (lisible sur falling) |
| g.rising[i] | bool | True pendant 1 cycle à l'activation de l'étape |
| g.falling[i] | bool | True pendant 1 cycle à la désactivation de l'étape |
| g.entrees[i] | bool | État brut de l'entrée (à remplir dans lire_entrees) |
| g.fm[i] | bool | Front montant d'entrée (True pendant 1 cycle) |
| g.fd[i] | bool | Front descendant d'entrée (True pendant 1 cycle) |
Fichiers de référence
Le moteur GRAFCET complet + l'exemple de référence qui démontre toutes les fonctionnalités.
⚙️ Le moteur GRAFCET complet
Conforme IEC 60848. Un seul fichier, aucune dépendance. La base pour tous les projets.
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. 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.
- Validation automatique des étapes sources (Règle 2)
- Franchissement simultané en deux passes (Règle 4 + Règle 5)
- Fronts d'étape
rising/fallinget fronts d'entréefm/fd - Timers
tempo, compteurscompt+compt_final - Étapes initiales multiples, réinitialisation (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]
🎯 L'ascenseur complet — exemple de référence
3 étapes, 3 transitions — mais TOUTES les fonctionnalités du moteur sont utilisées.
| 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 rising[1], RESET falling[2]). Buzzer bip 200 ms (rising[0] + tempo[0]). |
Front d'entrée fm | bpA en front montant (1 appui = 1 cycle), bpD pour l'AU |
Front d'étape rising | rising[0] : bip buzzer 200 ms ("système prêt"). rising[1] : SET clignoter. Compteur de cycles. |
Front d'étape falling | RESET clignoter + affiche compt_final |
Temporisation tempo | 500 ms d'anti-rebond au repos |
Compteur d'étape compt | Appuis bpA pendant la montée (remis à 0 auto) |
| Compteur inter-cycles | nb_cycles (variable Python) — maintenance après 5 |
| Réinitialisation | bpD = arrêt d'urgence → g.reinitialiser() |
| Validation auto | calculer_transitions() = réceptivités pures |
Fichiers nécessaires sur l'ESP32 : grafcet_complet.py, essential.py.
Console Thonny : numéro de cycle, compteur d'appuis, 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)
🎮 Démo 1 — 10 étapes : divergences, convergences, TouchPad
Un parcours pédagogique non-réaliste qui ajoute tout ce que l'ascenseur ne montre pas : divergence ET, convergence ET, divergence OU, convergence OU, et TouchPad.
| Étape | Rôle | Sorties |
|---|---|---|
| É0 | Veille | LED bleue respire (PWM sinus) |
| É1 | Démarrage (2 s) | LED bleue fixe — [M] SET led_rouge clignote |
| É2 + É6 | DIVERGENCE ET | T1 active É2 et É6 simultanément |
| É2 | Branche G | LED verte — DIVERGENCE OU → bpB ou bpC |
| É3 | Choix G-gauche (bpB) | NeoPixel rouge — compt[3] : appuis bpA |
| É4 | Choix G-droite (bpC) | NeoPixel bleu — compt[4] : appuis bpA |
| É5 | Fin branche G | Attente convergence ET |
| É6 | Branche D | LED jaune — fm tp1 |
| É7 | Suite D | NeoPixel tout vert — fm tp2 |
| É8 | Fin branche D | Attente convergence ET |
| É5 + É8 | CONVERGENCE ET | T8 franchie seulement si É5 ET É8 actives |
| É9 | Finale (3 s) | [M] RESET led_rouge — double bip — NeoPixel jaune |
| Fonctionnalité | Comment c'est utilisé |
|---|---|
| LED qui respire | PWM sinus en É0 : duty = int(511 + 511 * sin(tempo[0] * π / 1000)) |
| Divergence ET | T1 active É2 et É6 simultanément — les deux branches s'exécutent en parallèle |
| Convergence ET | T8 attend que É5 et É8 soient toutes deux actives — la validation est automatique (Règle 2) |
| Divergence OU | T2 (bpB) et T3 (bpC) partent de É2 — réceptivités exclusives, une seule branche s'active |
| Convergence OU | T4 et T5 mènent toutes deux à É5 — la première arrivée gagne, É5 attend la convergence ET |
TouchPad fm[2] / fm[3] | tp1 et tp2 détectés en front montant — un effleurement = un franchissement |
compt[3] / compt[4] | Compte les appuis bpA pendant l'étape — compt_final lu sur falling |
| Compteur inter-cycles | nb_parcours (variable Python) incrémenté sur rising[0] — initialisé à -1 pour ignorer le démarrage |
Fichiers nécessaires sur l'ESP32 : grafcet_complet.py, essential.py.
Console Thonny : appuis bpA par branche, compteur de parcours, arrêt d'urgence.
demo_1.py — Démo 10 étapes : divergences, convergences, TouchPad .python avance/grafcet/demo_1.py 294 lignes
GitHub
# =============================================================================
# demo_complet.py — Démonstration GRAFCET complète (10 étapes)
# =============================================================================
# Exemple pédagogique utilisant TOUTES les fonctionnalités du moteur
# grafcet_complet.py sur carte ENIM. Pas de système réaliste — c'est
# un parcours qui montre chaque feature du GRAFCET.
#
# FONCTIONNALITÉS DÉMONTRÉES :
#
# ┌─────────────────────────────────────────────────────────────────────┐
# │ Fonctionnalité │ Où dans le code │
# ├─────────────────────────────┼──────────────────────────────────────┤
# │ LED qui respire (PWM) [C] │ led_bleue PWM sinus en étape 0 │
# │ LED qui clignote [M] │ led_rouge 2 Hz (étapes 1 à 8) │
# │ Mode CONTINU [C] │ led_bleue, led_verte, led_jaune, │
# │ │ NeoPixel │
# │ Mode MÉMORISÉ [M] │ led_rouge : SET rising[1], │
# │ (SET/RESET) │ RESET rising[9]. Buzzer rising[0]. │
# │ Front montant d'entrée (fm) │ bpA=fm[0], bpD=fm[1], tp1=fm[2], │
# │ │ tp2=fm[3] │
# │ Front montant d'étape │ rising[0]=bip, rising[1]=SET, │
# │ │ rising[9]=RESET │
# │ Front descendant d'étape │ falling[3]/falling[4]=affiche compt │
# │ Temporisation (tempo) │ tempo[0]>500, tempo[1]>2000, │
# │ │ tempo[3]>1500, tempo[4]>1500, │
# │ │ tempo[9]>3000 │
# │ Compteur d'étape (compt)[C] │ compt[3] et compt[4] : appuis bpA │
# │ Divergence ET │ T1 → étapes 2 et 6 en parallèle │
# │ Convergence ET │ T8 ← étapes 5 et 8 simultanées │
# │ Divergence OU │ T2 (bpB) / T3 (bpC) depuis étape 2 │
# │ Convergence OU │ T4/T5 → étape 5 │
# │ TouchPad │ tp1 (T6), tp2 (T7) │
# │ Réinitialisation │ bpD (fm[1]) → reinitialiser() │
# │ Validation auto (Règle 2) │ réceptivités pures │
# └─────────────────────────────┴──────────────────────────────────────┘
#
# ENTRÉES :
# bpA (Pin 25) → Start + comptage (front montant fm[0])
# bpB (Pin 34) → Divergence OU : choix gauche (niveau)
# bpC (Pin 39) → Divergence OU : choix droit (niveau)
# bpD (Pin 36) → Arrêt d'urgence (front montant fm[1])
# tp1 (Pin 15) → TouchPad 1 : branche droite (front montant fm[2])
# tp2 (Pin 4) → TouchPad 2 : suite droite (front montant fm[3])
#
# SORTIES :
# led_bleue (Pin 2) → respire en veille (PWM), fixe sinon [CONTINU]
# led_verte (Pin 18) → branche gauche [CONTINU]
# led_jaune (Pin 19) → branche droite [CONTINU]
# led_rouge (Pin 23) → clignote 2 Hz étapes 1-8 [M] SET/RESET
# buzzer (Pin 5) → bip au démarrage/retour + double bip [M] one-shot
# NeoPixel (Pin 26) → indicateur de choix/progression [C]
#
# GRAFCET (10 étapes) :
#
# Étape 0 — Veille
# [C] LED bleue respire (PWM sinus)
# [M] Bip buzzer 200 ms (rising[0])
# │ T0 : fm[0] + tempo > 500
# Étape 1 — Démarrage
# [C] LED bleue fixe ON
# [M] SET led_rouge clignote (rising[1])
# │ T1 : tempo > 2000 → DIVERGENCE ET
# ├──────────────────────────────────────────┐
# Étape 2 — Branche G Étape 6 — Branche D
# [C] LED verte ON [C] LED jaune ON
# │ DIVERGENCE OU │ T6 : fm tp1
# ├───────────┐ Étape 7 — Suite D
# T2:bpB T3:bpC [C] NeoPixel tout vert
# │ │ │ T7 : fm tp2
# Étape 3 Étape 4 Étape 8 — Fin D
# [C] NP rouge [C] NP bleu (attente conv. ET)
# [C] compt++ [C] compt++ │
# │ T4:1.5s │ T5:1.5s │
# └───────────┘ │
# CONVERGENCE OU │
# │ │
# Étape 5 — Fin G │
# (attente conv. ET) │
# └──────── T8 : CONVERGENCE ET ─────────────┘
# │
# Étape 9 — Finale
# [M] RESET led_rouge (rising[9])
# [M] Buzzer double bip (rising[9])
# [C] NeoPixel tout jaune
# │ T9 : tempo > 3000 → retour Étape 0
#
# Légende : [C] = continu (suit l'étape) [M] = mémorisé (SET/RESET)
#
# À tout moment : fm[1] (bpD) → reinitialiser()
#
# Fichiers nécessaires sur l'ESP32 :
# grafcet_complet.py, essential.py
# =============================================================================
from machine import Pin, PWM
from math import sin, pi
from time import ticks_ms
from grafcet_complet import Grafcet
from essential import (
synchro_ms,
bpA, bpB, bpC, bpD,
tp1, tp2,
led_bleue, led_verte, led_jaune, led_rouge,
buzzer, np,
)
# LED bleue en PWM pour la respiration
pwm_bleue = PWM(Pin(2), freq=1000, duty=0)
# =============================================================================
# GRAFCET — 10 étapes
# =============================================================================
# nb_fronts=4 : bpA(0), bpD(1), tp1(2), tp2(3)
g = Grafcet(nb_etapes=10, nb_fronts=4)
T = [
(0, (0,), (1,)), # T0 : Veille → Démarrage
(1, (1,), (2, 6)), # T1 : Démarrage → DIVERGENCE ET (branches G et D)
(2, (2,), (3,)), # T2 : bpB → Choix Gauche (DIVERGENCE OU)
(3, (2,), (4,)), # T3 : bpC → Choix Droit (DIVERGENCE OU)
(4, (3,), (5,)), # T4 : Choix G → Fin gauche
(5, (4,), (5,)), # T5 : Choix D → Fin gauche (CONVERGENCE OU)
(6, (6,), (7,)), # T6 : tp1 → Suite droite
(7, (7,), (8,)), # T7 : tp2 → Fin droite
(8, (5, 8), (9,)), # T8 : CONVERGENCE ET → Finale
(9, (9,), (0,)), # T9 : Finale → retour Veille
]
transitions = [False] * len(T)
# =============================================================================
# VARIABLES
# =============================================================================
clignoter = False # LED rouge clignote (mémorisé SET/RESET)
nb_parcours = -1 # compteur de parcours (-1 : le rising[0] du démarrage ne compte pas)
# NeoPixel éteint au démarrage
for i in range(8):
np[i] = (0, 0, 0)
np.write()
# =============================================================================
# CYCLE GRAFCET
# =============================================================================
def gerer_actions():
global clignoter, nb_parcours
# --- LED bleue : respire en veille (PWM sinus), fixe sinon [CONTINU] ---
if g.etapes[0]:
# Respiration : duty varie sinusoïdalement, période 2 secondes
duty = int(511 + 511 * sin(g.tempo[0] * pi / 1000))
pwm_bleue.duty(duty)
elif g.etapes[1]:
pwm_bleue.duty(1023) # fixe ON pendant le démarrage
else:
pwm_bleue.duty(0) # éteinte sinon
# --- LED verte : branche gauche [CONTINU] ---
led_verte.value(g.etapes[2] or g.etapes[3] or g.etapes[4])
# --- LED jaune : branche droite [CONTINU] ---
led_jaune.value(g.etapes[6] or g.etapes[7])
# --- Bip buzzer "système prêt" (rising[0]) [MÉMORISÉ] ---
if g.rising[0]:
buzzer.init(freq=1000, duty=50)
if g.etapes[0] and g.tempo[0] > 200:
buzzer.deinit()
# --- LED rouge clignote (SET/RESET — traverse étapes 1 à 8) [MÉMORISÉ] ---
if g.rising[1]: clignoter = True # SET : début du parcours
if g.rising[9]: clignoter = False # RESET : arrivée en finale
# --- Buzzer double bip en finale [MÉMORISÉ] ---
if g.rising[9]:
buzzer.init(freq=1500, duty=50)
if g.etapes[9] and g.tempo[9] > 100:
buzzer.deinit()
if g.etapes[9] and g.tempo[9] > 300:
buzzer.init(freq=1500, duty=50)
if g.etapes[9] and g.tempo[9] > 400:
buzzer.deinit()
# --- NeoPixel : indicateur visuel selon l'étape [CONTINU] ---
if g.rising[3]:
for i in range(4): np[i] = (30, 0, 0) # choix G : rouge à gauche
for i in range(4, 8): np[i] = (0, 0, 0)
np.write()
if g.rising[4]:
for i in range(4): np[i] = (0, 0, 0)
for i in range(4, 8): np[i] = (0, 0, 30) # choix D : bleu à droite
np.write()
if g.rising[7]:
for i in range(8): np[i] = (0, 30, 0) # suite droite : tout vert
np.write()
if g.rising[9]:
for i in range(8): np[i] = (30, 30, 0) # finale : tout jaune
np.write()
if g.rising[0]:
for i in range(8): np[i] = (0, 0, 0) # veille : tout éteint
np.write()
# --- Compteurs d'étape : appuis bpA pendant les choix ---
if g.etapes[3] and g.fm[0]:
g.compt[3] += 1
if g.etapes[4] and g.fm[0]:
g.compt[4] += 1
if g.falling[3]:
print(" Appuis bpA pendant choix gauche :", g.compt_final[3])
if g.falling[4]:
print(" Appuis bpA pendant choix droit :", g.compt_final[4])
# --- Compteur de parcours ---
if g.rising[0]:
nb_parcours += 1
if nb_parcours > 0:
print("Parcours", nb_parcours, "terminé")
def affecter_sorties():
# LED rouge : clignotement 2 Hz (mémorisé)
if clignoter:
led_rouge.value(ticks_ms() % 500 < 250)
else:
led_rouge.value(0)
def lire_entrees():
# Fronts d'entrée : bpA(0), bpD(1), tp1(2), tp2(3)
g.entrees[0] = bpA.value()
g.entrees[1] = bpD.value()
g.entrees[2] = tp1.read() < 350 # TouchPad touché
g.entrees[3] = tp2.read() < 350 # TouchPad touché
def calculer_transitions():
# Réceptivités pures — le moteur vérifie les étapes sources (Règle 2)
transitions[0] = g.fm[0] and (g.tempo[0] > 500) # front bpA + anti-rebond
transitions[1] = g.tempo[1] > 2000 # attente 2 secondes
transitions[2] = bpB.value() # divergence OU : gauche
transitions[3] = bpC.value() # divergence OU : droite
transitions[4] = g.tempo[3] > 1500 # fin choix gauche (1.5s)
transitions[5] = g.tempo[4] > 1500 # fin choix droit (1.5s)
transitions[6] = g.fm[2] # front tp1 (toucher)
transitions[7] = g.fm[3] # front tp2 (toucher)
transitions[8] = True # convergence ET (validation auto)
transitions[9] = g.tempo[9] > 3000 # fin finale (3s)
# =============================================================================
# BOUCLE PRINCIPALE
# =============================================================================
print("=== GRAFCET Démonstration complète (10 étapes) ===")
print("bpA=Start bpB=Choix G bpC=Choix D bpD=AU")
print("tp1=Branche droite tp2=Suite droite")
print()
print("Parcours : Veille → Démarrage (2s)")
print(" Branche G : bpB ou bpC → choix (1.5s)")
print(" Branche D : tp1 → tp2")
print(" Convergence ET → Finale (3s) → retour Veille")
print()
while True:
g.franchir(T, transitions)
# Arrêt d'urgence
if g.fm[1]:
g.reinitialiser()
clignoter = False
nb_parcours = -1
led_rouge.value(0)
buzzer.deinit()
pwm_bleue.duty(0)
for i in range(8): np[i] = (0, 0, 0)
np.write()
print("!!! ARRÊT D'URGENCE !!!")
g.tick(20)
gerer_actions()
affecter_sorties()
lire_entrees()
g.detecter_fronts_entrees()
calculer_transitions()
synchro_ms(20)
🖥️ Démo 2 — 12 étapes : OLED, chenillard, fd, meilleur temps
La démo 1 étendue avec deux étapes supplémentaires et l'OLED.
Met en évidence les features qui manquent encore : front descendant fd,
action mémorisée sur plusieurs étapes, sauvegarde de tempo.
Les deux nouvelles étapes par rapport à la démo 1 :
| Étape | Rôle | Sorties |
|---|---|---|
| É0 → É9 | Identiques à la démo 1 + OLED sur chaque étape | |
| É10 | Chenillard (3 s) | NeoPixel 1 LED orange tourne (150 ms) — OLED barre rebondissante (période 2 s) |
| É11 | Bilan (2 s) | OLED : nb parcours + meilleur temps G — relâcher bpA (fd[0]) pour skipper |
| Fonctionnalité | Comment c'est utilisé |
|---|---|
| OLED dynamique | Countdown seconde par seconde en É1. Labels "Branche G / D". Barre rebondissante en É10. Bilan en É11. |
| Chenillard mémorisé | chenillard_actif : SET sur rising[9], RESET sur rising[11] — traverse É9, É10 et É11 |
| NeoPixel chenillard | pos = (g.tempo[10] // 150) % 8 — 1 LED orange qui tourne, calculé à partir du timer de l'étape |
Front descendant fd[0] | Relâcher bpA en É11 franchit T11 immédiatement — sans attendre les 2 s |
Sauvegarde de tempo | franchir() remet tempo[i] à 0 avant que falling soit lisible. Il faut sauvegarder tempo[i] dans une variable Python à chaque cycle où l'étape est active. |
| Meilleur temps | meilleur_temps_G mis à jour sur falling[3] et falling[4] à partir de la valeur sauvegardée — affiché dans le bilan OLED |
Le piège du tempo sur falling
franchir() remet tempo[i] à zéro dans la même passe qu'il pose falling[i].
Résultat : quand gerer_actions() lit falling[3], tempo[3] vaut déjà 0.
_tempo3_sauve = 0
def gerer_actions():
global _tempo3_sauve
if g.etapes[3]:
_tempo3_sauve = g.tempo[3] # mis à jour chaque cycle
if g.falling[3]:
meilleur = _tempo3_sauve # tempo réel avant reset (±20 ms) Fichiers nécessaires sur l'ESP32 : grafcet_complet.py, essential.py, ssd1306.py.
Console Thonny : appuis bpA par branche, meilleur temps G, compteur de parcours, arrêt d'urgence.
demo_2.py — Démo 12 étapes : OLED, chenillard, fd, meilleur temps .python avance/grafcet/demo_2.py 3 lignes
GitHub
# Fichier : avance/grafcet/demo_2.py
# Repo : vincent-herm/fablab-esp32-ateliers
# Le fichier sera disponible une fois le repo créé sur GitHub.Pièges et bonnes pratiques
fm traverse les transitions
Le fm[0] qui déclenche une transition est encore True quand l'étape suivante s'active. Ne pas compter avec fm dans l'étape qui vient d'être activée par ce même front. Compter dans une étape plus tardive.
compt vs variable Python
g.compt[i] compte dans UNE étape (remis à 0 à la désactivation). Pour compter ENTRE les cycles, utiliser une variable Python classique. Pour lire la valeur au moment de la désactivation, utiliser g.compt_final[i] sur g.falling[i].
Divergence en OU
Si deux transitions partent de la même étape, leurs réceptivités doivent être exclusives. Le moteur ne gère pas de priorité — si les deux sont vraies, les deux branches s'activent.
rising au démarrage (conforme IEC 60848)
Le moteur pose rising sur les étapes initiales au démarrage et après reinitialiser(). C'est conforme à la norme : l'étape initiale "devient active" = événement d'activation (P1). Si tu utilises rising[0] pour compter des cycles, le démarrage compte comme un cycle — initialise le compteur à -1 pour l'ignorer.
Buzzer PWM inactif après essential.py
essential.py appelle buzzer.deinit() à l'import. Pour utiliser le buzzer : buzzer.init(freq=1000, duty=50) (pas buzzer.freq() qui crashe). Pour couper : buzzer.deinit().
Arrêt d'urgence
L'AU est testé dans la boucle principale, en dehors du GRAFCET. C'est un raccourci acceptable. Le GEMMA (cours futur) gérera les modes de marche/arrêt proprement avec un GRAFCET hiérarchique. Le bip buzzer sonne aussi après AU (= "système prêt à redémarrer").
tempo non lisible sur falling
franchir() remet tempo[i] à 0 dans la même passe qu'il pose falling[i]. Résultat : dans gerer_actions(), quand falling[i] est True, tempo[i] vaut déjà 0. Contrairement à compt_final[i] qui est sauvegardé automatiquement par le moteur, il n'existe pas de tempo_final.
Solution : sauvegarder tempo[i] dans une variable Python à chaque cycle où l'étape est active. La valeur est disponible avec une précision de ±20 ms.
_tempo_sauve = 0
def gerer_actions():
global _tempo_sauve
if g.etapes[3]:
_tempo_sauve = g.tempo[3] # mis à jour chaque cycle
if g.falling[3]:
print("durée :", _tempo_sauve, "ms") # tempo réel avant reset Créer ton propre projet
Garde grafcet_complet.py intact. Crée ton propre fichier avec :
- Ta table T (structure du GRAFCET)
- Tes 4 fonctions :
gerer_actions,affecter_sorties,lire_entrees,calculer_transitions - La boucle principale avec les 7 phases
from grafcet_complet import Grafcet
g = Grafcet(nb_etapes=4, nb_fronts=2)
T = [
(0, (0,), (1,)),
(1, (1,), (2, 3)), # divergence ET
(2, (2, 3), (0,)), # convergence ET
]
transitions = [False] * len(T)
# ... tes 4 fonctions ...
while True:
g.franchir(T, transitions)
g.tick(20)
gerer_actions()
affecter_sorties()
lire_entrees()
g.detecter_fronts_entrees()
calculer_transitions()
synchro_ms(20) Pour aller plus loin
Le module NeoPixel progressif ajoute un affichage visuel fluide au même ascenseur.
Le GEMMA introduira la gestion des modes de marche et d'arrêt via un GRAFCET hiérarchique.