Synchronisation temps réel et bus d'événements
Le pilotage domotique de leukOS utilise une architecture hybride : REST admet la commande, puis WebSocket diffuse son cycle de vie. Le daemon C++ reste l'autorité pour l'ordonnancement, l'exécution par le MCU et la persistance SQLite. QML, Vue.js et Android consomment le même contrat d'événement et appliquent le même comportement visuel.
Cette séparation évite de maintenir une requête HTTP ouverte pendant le traitement du matériel et permet à plusieurs interfaces de recevoir simultanément le même état.
Architecture
Le hub Go ne transforme pas les événements : il expose un proxy WebSocket pour les clients web. Les clients locaux QML et Android peuvent se connecter directement au daemon.
Cycle d'une commande
Une commande est retirée de la file après un succès ou un échec définitif. Les erreurs
intermédiaires restent associées au même event_id.
Contrat d'événement
Toutes les interfaces reçoivent le même objet JSON :
{
"event_type": "command_status",
"event_id": "1792093845123-42",
"domain": "module",
"target": "m_001122aabbcc",
"action": "on",
"status": "retrying",
"attempt": 2,
"max_attempts": 4,
"timestamp_ms": 1792093845123,
"error": "relay command rejected"
}
| Champ | Description |
|---|---|
event_type | Type stable, actuellement command_status. |
event_id | Identifiant unique conservé pendant toute la commande. |
domain | Famille de cible : module, relay ou valve. |
target | Identifiant stable permettant au composant UI de reconnaître l'événement. |
action | Action demandée : par exemple on, off, open ou close. |
status | État courant du traitement. |
attempt | Numéro de la tentative courante ; 0 pendant pending. |
max_attempts | Tentative initiale plus le nombre de retries autorisés. |
timestamp_ms | Horodatage Unix en millisecondes. |
error | Message optionnel présent lors de retrying ou failed. |
États
| État | Signification | Comportement de l'interface |
|---|---|---|
pending | La commande est acceptée et placée dans la file. | Loader et bouton bloqué. |
processing | Le dispatcher appelle le matériel. | Loader conservé. |
retrying | Une tentative a échoué, une autre est planifiée. | Loader et progression conservés. |
succeeded | Le matériel a accepté et appliqué la commande. | Fin du pending et état actualisé. |
failed | Toutes les tentatives ont échoué. | Bouton réactivé et erreur dans le label d'état. |
REST et WebSocket
Les commandes utilisent toujours les routes REST existantes. La réponse HTTP signifie uniquement que la commande a été admise dans le dispatcher :
{
"ok": true,
"event_id": "1792093845123-42",
"status": "pending"
}
Elle ne prouve pas que le MCU a exécuté l'action. Seuls les événements WebSocket
succeeded ou failed sont définitifs.
| Endpoint | Rôle |
|---|---|
GET /events | Upgrade WebSocket direct sur le daemon C++. |
GET /api/domotics/events | Proxy WebSocket du hub Go pour l'IHM Vue.js. |
GET /command-event/{event_id} | Relecture durable de l'état courant depuis SQLite. |
GET /health | Santé de SQLite, du dispatcher et taille de la file. |
GET /domotics/settings | Lecture des paramètres du dispatcher. |
POST /domotics/settings | Application et persistance des paramètres. |
Gestion du bouton et du timeout
Lors d'un appui ON/OFF :
- le composant se désactive immédiatement pour empêcher les rafales ;
- un délai de 10 secondes démarre ;
- le composant attend un événement
pending,processingouretryingqui prouve que la commande est prise en charge ; - après cet accusé, le composant affiche un loader et reste désactivé jusqu'à l'état final ;
- sans accusé après 10 secondes, le bouton est réactivé et le label habituellement utilisé pour « Allumé / Éteint » affiche l'erreur ;
- en cas de
failed, le même label affiche le message transmis par le dispatcher, sans masquer ni supprimer la carte.
Pour éviter un faux timeout lors d'une reconnexion WebSocket, la réponse REST associe
la commande à son event_id. Si aucun événement temps réel n'est reçu après une
seconde, le client relit GET /command-event/{event_id}. Cette reprise consulte le même
état persisté par le dispatcher et continue chaque seconde jusqu'à l'accusé ou au délai
final de 10 secondes. Un contrôle n'accepte par ailleurs aucun nouvel appui tant que le
handshake du flux WebSocket n'est pas terminé.
Le délai de 10 secondes concerne uniquement l'accusé de mise en attente. Une commande déjà reconnue peut rester en traitement pendant les retries sans être libérée par ce timer.
Retry et configuration
Les paramètres sont accessibles dans Configuration → Domotique :
| Paramètre | Valeur par défaut | Signification |
|---|---|---|
retry_count | 3 | Nombre de nouvelles tentatives après la tentative initiale. |
retry_interval_s | 5 | Délai entre deux tentatives, en secondes. |
Avec la configuration par défaut, une commande peut donc être exécutée au maximum quatre fois. Les valeurs sont appliquées au dispatcher actif et persistées dans SQLite.
Exemple de configuration :
{
"domotics": {
"retry_count": 3,
"retry_interval_s": 5,
"pending_timeout_s": 10
}
}
pending_timeout_s formalise le contrat commun ; les interfaces utilisent actuellement
la valeur imposée de 10 secondes.
Persistance SQLite
Le dispatcher écrit chaque transition avant de la publier sur le bus :
command_eventsconserve le dernier état connu de chaqueevent_id;eventsconserve le journal chronologique des transitions ;domotics_settingsconserve les paramètres du dispatcher.
Un succès et un échec définitif sont ainsi consultables après redémarrage. Si SQLite ne
répond plus, le dispatcher devient non sain et /health retourne un échec.
Bus côté interfaces
Qt/QML
NodeClient maintient la connexion WebSocket, décode les frames et émet un signal
commandEvent. La page de la pièce utilise Connections pour router l'événement vers
la carte dont le target correspond.
Vue.js
Le client Web expose un bus publish/subscribe. La connexion au proxy du hub publie
chaque événement reçu ; EntityCard s'abonne lors de son montage et se désabonne lors
de sa destruction.
Android
DomoticsEventBus maintient la socket et un ensemble de listeners. Le repository mappe
le target backend vers l'identifiant de la vue, tandis que RoomScreen affiche le
pending, le loader ou l'erreur.
Dans les trois cas, le composant ne dépend pas d'un callback spécifique à sa requête : il réagit à tout événement portant sa cible. Cela garantit la cohérence lorsqu'une commande provient d'une autre interface.
Watchdog et reprise
Le watchdog applicatif interroge /health toutes les cinq secondes et contrôle aussi
la présence du daemon. Après trois échecs consécutifs, il redémarre
S90leukos-node. Un verrou empêche le lancement simultané de plusieurs watchdogs.
Le daemon ne nourrit le watchdog matériel que si SQLite et le dispatcher sont sains. Le mécanisme couvre donc notamment :
- un daemon arrêté ou bloqué ;
- une base SQLite inaccessible ;
- un dispatcher qui ne parvient plus à journaliser les transitions.
Le watchdog ne rejoue pas arbitrairement une commande matérielle après redémarrage : les événements persistés servent d'audit, ce qui évite une double activation dangereuse.
Vérifications d'exploitation
La santé courante peut être contrôlée avec :
curl -fsS http://127.0.0.1:8080/health
Réponse saine :
{
"ok": true,
"sqlite": true,
"dispatcher": true,
"queue_size": 0
}
Le scénario de validation du retry doit faire apparaître la séquence suivante dans SQLite et sur WebSocket :
pending
processing (tentative 1)
retrying
processing (tentative 2)
retrying
processing (tentative 3)
retrying
processing (tentative 4)
failed ou succeeded
Fichiers de référence
nodes/agent/include/event_dispatcher.hppnodes/agent/src/event_dispatcher.cppnodes/agent/src/local_api.cppnodes/agent/src/local_db.cppnodes/agent/packaging/bin/leukos-node-watchdognodes/ui/src/node/node_client.cppnodes/ui/qml/pages/domotics/RoomDevicesPage.qmldomotic/hub/internal/api/ws.godomotic/web/src/api.jsmobile/android/app/src/main/java/com/leukos/domotics/network/DomoticsEventBus.java