T1 Architecture & flux

Topologie globale et détail des 4 flux applicatifs
Les quatre flux de données Ce qui circule, et dans quel ordre
Anatomie d'une trame Ce que devient une mesure, de la radio à la plateforme
Composants du flux Les 187 nodes, par rôle
Stockage local Ce que la passerelle écrit, et ce qu'elle relit
Résilience Ce qui résiste à une coupure, et ce qui ne résiste pas

Vue d'ensemble

La CloudGate CG8102 est à la fois la gateway LoRa (packet forwarder), le LoRaWAN Network Server et l'Application Server. Elle remplace une combinaison gateway + LNS cloud classique. Tout le code applicatif est implanté dans le canvas LuvitRED (port Lua/Luvit de Node-RED, embarqué dans le firmware CG).

📡
Balise Asystom #1
Sentinel · FPort 1/4 uplink
FPort 64/70 downlink
📡
Balise #2 .. #N
jusqu'à 30 par CG
Class A LoRaWAN
📦
CloudGate CG8102
LuvitRED canvas · Lua/Luvit
LoRa NS + AS · LTE/Ethernet
Stockage local 15 j
file out append
💾
Audit local
/mnt/data/luvitred/
uplinks-YYYY-MM-DD.jsonl
rétention 15 j
HTTPS · Bearer DAP
☁️
IoBase (Indaba)
data lake
POST /v1/payloads
POST /gateway/health
HTTPS · Basic + X-Network
☁️
Asystom Advisor
analyse vibratoire
POST /lorawan (uplinks)
GET /getPendingMsgs (dl)

Les quatre flux de données

FLUX 01
Payload brute capteurs → IoBase
Chaque trame LoRa reçue est transformée en POST /v1/payloads avec Bearer DAP. Body au format spec v1.0 : equipment_model, payload (hex), site, datasource, metrics[], timestamp ISO 8601. Headers X-CloudGate-Id + X-Device-Id. Voir Chap. 03.
FLUX 02
Payload brute capteurs → Asystom Advisor
Mêmes trames, en parallèle, vers Asystom. POST /lorawan avec Basic auth + header X-Network. Body Loriot-like : endDevice.devEui, fPort, fCntUp, payload, recvTime, dataRate, gwInfo[]. Voir Chap. 04.
FLUX 03
Asystom downlink polling → balises
Toutes les 60s, GET /getPendingMsgs?client=<id>. Si la réponse contient des device_list/payload/cmd, la chaîne parse_asystom_dl émet un msg.app = { queue = { [DevEUI] = { data, fport, confirmed=false } } } vers le node lora-app qui en fait un downlink LoRa Class A.
FLUX 04
Santé CloudGate → IoBase
Heartbeat 1×/h + 1× par event capteur. POST /gateway/health. Body : life_counter, uptime, lora_uplinks_count, lora_status (active/idle/stale), gsm_rssi, gsm_operator, gsm_technology, gsm_rfband, modem_model, last_iobase_ok_ts, last_asystom_ok_ts.

Anatomie d'une trame uplink

Une trame LoRa reçue est fan-out en 3 destinations parallèles depuis la CloudGate : IoBase (cloud), Asystom Advisor (cloud), et un audit local sur disque. Cliquer Lancer pour visualiser le timing.

📡
Balise Asystom
Sentinel · FPort 4
📦
CloudGate
on_lora_message · fan-out 3 destinations
file out append
💾
Audit local
/mnt/data/luvitred/
uplinks-YYYY-MM-DD.jsonl
HTTPS Bearer DAP
☁️
IoBase
POST /v1/payloads
HTTPS Basic + X-Network
☁️
Asystom Advisor
POST /lorawan
— prêt —

Composants du flux

Le flux compte aujourd'hui 187 nodes, dont 67 fonctions. Les quatre flux de données ci-dessus en sont le cœur ; le reste assure le pilotage, la persistance et l'observabilité.

CatégorieNodesRôle
Entrée LoRa lora-srv · lora-app · un node lora device par capteur déclaré Packet forwarder + LNS + AppServer + flow-node device-spécifique. Sortie vers on_lora_message.
Dispatcher on_lora_message (function) Compteurs + 5 wires sortantes : lora out, build_iobase, build_audit, build_asystom, build_health.
Flux 01 IoBase build_iobase_msg → iobase POST → handle_iobase_resp Construit le corps et les en-têtes, envoie avec le jeton. Un échec est journalisé — la mesure n'est pas rejouée.
Flux 02 Asystom build_asystom_msg → asystom POST → handle_asystom_resp Build body Loriot-like + Basic auth + X-Network header, POST.
Flux 03 DL polling asystom downlink poll (cron 60s) → build_asystom_dl_get → http GET → parse_asystom_dl → lora-app Interroge la plateforme, analyse la réponse et construit un seul message dont la file porte tous les équipements concernés, indexés par DevEUI.
Flux 04 Health health hourly (cron 3600s) + on_lora_message → build_health_msg → health POST → handle_health_resp Body 11 champs métriques. POST 1×/h + à chaque event capteur.
Audit local build_audit_line → audit jsonl (file out append) + audit rotate daily 02:00 (cron) → audit_rotate → file out delete JSONL append per day dans /mnt/data, cleanup > 15 jours quotidien.
DAP auth 3 inject + 3 build + dap POST + handle_dap_resp + persist_dap + save dap state + chaîne restore au boot Register/Token/refresh single-use protégé par mutex. Persistance flash. Voir Chap. 02.
Heartbeat life life tick 1s → life_tick → life counter file out + load life @startup → file in → life_restore Compteur 0→32000 cyclique persisté toutes les 60s, restauré au boot.
CG métriques cg admin login + cg metrics poll (cron 300s) → 3 endpoints (internet/diag wwan0/sim) → handle_cg_metrics Récupère GSM RSSI, opérateur, technology, RF band depuis l'admin local CG.
File de rejeu aucune Le node de configuration a été retiré (boucle de plantage). Les mesures ne sont pas rejouées.
Cockpit embarqué 2 serveurs HTTP (ports 8888 et 8889) · 13 routes · cockpit_login, cockpit_health, cockpit_balises, cockpit_globals, post_meta, cockpit_downlink… Sert l'interface et son API depuis la passerelle. Aucun service externe. Voir Chap. 05.
Statistiques horaires on_lora_message → uplink_hourly · persist_uplink_stats (toutes les 10 min) · restore_uplink_stats au boot Compteurs par heure et par balise sur 24 h glissantes, dans leur propre fichier. Alimentent les graphiques du tableau de bord.
Ajout de capteur post_balise → sd_login_req → sd_cookie_getflows → sd_backup → sd_post_prep → réécriture du flux La passerelle se déclare elle-même un capteur en réécrivant son propre flux — ce qui redémarre toutes les chaînes. Protégé par plusieurs garde-fous.
Supervision externe build_supervision_ping · build_slack_heartbeat Deux chemins de sortie vers l'extérieur, l'un à chaque rafraîchissement de jeton, l'autre périodique. Les deux sont débranchés : les nodes existent, leur déclencheur n'a plus de fil. Ils sont là pour être recâblés vers la supervision d'un exploitant, avec son URL dans la configuration du site. En l'état, la passerelle n'émet rien vers un tiers.
Init startup init → startup_init Set globals au boot : start_time, hosts, credentials, defaults.

Stockage local

Ce que la passerelle écrit sur sa propre mémoire. Tous ces fichiers appartiennent à root : le compte d'administration ne peut pas les modifier directement, ce qui explique pourquoi les mises à jour passent par /tmp puis par le flux lui-même.

FichierContenuÉcritRelu
dap_state.json Authentification de l'environnement principal : code d'appareil, jetons, horodatage, adresse et identifiants. à chaque renouvellementau démarrage
dap_state_b.json Idem pour le second environnement. Sans sa relecture au démarrage, le jeton — à usage unique — serait perdu à chaque redémarrage. à chaque renouvellementau démarrage
balises_meta.json Configuration des balises : nom, cadence, site, source de données, modèle, métriques, type, environnement de destination. depuis le cockpitau démarrage et à chaque écriture
balises_state.json État de service : dernière trame, compteurs, qualité radio, réglage en cours, historique récent. toutes les 2 min si modifiéau démarrage
uplink_hourly.json Compteurs par heure et par balise sur 24 h glissantes. Alimente les graphiques. toutes les 10 min si modifiéau démarrage
globals_override.json Variables saisies au cockpit. Priment sur la copie conservée dans les fichiers d'authentification. depuis le cockpitau démarrage
life_counter.json Compteur de vie, qui prouve que le flux tourne. périodiquementau démarrage
cockpit_*.html/js/css Les fichiers de l'interface, servis par le flux. au déploiement de l'interfaceà chaque requête
uplinks-AAAA-MM-JJ.jsonl Journal des trames reçues, une ligne par trame. C'est une trace, pas une file : rien n'est renvoyé depuis ce fichier. à chaque tramejamais par le flux
flow_pre_write.json Sauvegarde du flux avant qu'il se réécrive lui-même, lors de l'ajout d'un capteur. avant réécritureà la main, en cas de besoin
queue/ Vestige de la file de rejeu retirée du flux. Répertoire inutilisé. ——
L'arriéré des journaux n'est pas rattrapé

Relevé le 4 septembre 2026 : 63 journaux pour 13,9 Mo, dont certains remontent à septembre 2025 — alors que la rétention annoncée est de quinze jours. La rotation ne traite qu'un fichier par exécution, celui qui vient d'atteindre l'âge limite ; elle ne rattrape pas ce qui s'est accumulé pendant qu'elle était en panne.

Ce n'est pas urgent — la partition est occupée à 29 %, avec 68 Mo libres — mais l'arriéré ne disparaîtra pas tout seul. Le nettoyage se fait à la main, en supprimant les journaux antérieurs à la fenêtre voulue.

Un second espace, /overlay/etc/luvitred/store/lora_db/, contient la base du réseau LoRa : sessions, clés et compteurs des capteurs rattachés. Il est géré par la pile radio, pas par le flux — mais c'est lui qui fait qu'un capteur reste rattaché après un redémarrage.

Résilience

Coupure WAN courte
Les mesures émises pendant la coupure sont perdues. Voir l'encadré ci-dessous.
Coupure WAN longue (> 1 h)
Le retour du réseau est détecté par cg metrics poll, qui déclenche un renouvellement préventif du jeton. Les mesures manquées ne sont pas rattrapées.
Token DAP expiré
POST → 401 → fn_trigger_refresh_on_401 → build_dap_refresh (mutex) → nouveau jeton. La trame qui a essuyé le 401 est perdue ; les suivantes passent.
Reboot CG
Tokens restaurés depuis dap_state.json au boot. life_counter restauré. Aucun re-enrôlement administrateur nécessaire.
Refresh concurrent
Mutex global.dap_refreshing → 1 seul appel /refresh à la fois. Protège le single-use du refresh_token.
Journal local des trames
Une ligne JSON par trame reçue dans uplinks-AAAA-MM-JJ.jsonl, conservée 15 jours. C'est une trace, pas une file : rien n'est renvoyé depuis ce fichier.
Il n'y a pas de file de rejeu sur cette passerelle

Une mesure qui n'atteint pas sa destination — coupure réseau, plateforme injoignable, jeton expiré au mauvais moment — est perdue. Elle est journalisée sur la passerelle, mais rien ne la renvoie.

Le node de configuration de file a été retiré du flux le 2 août 2026 : il provoquait une boucle de plantage. Deux requêtes sortantes (health POST et asystom POST) portent encore un queueenabled à vrai pointant vers une file qui n'existe pas — configuration orpheline, sans effet, à nettoyer.

Conséquence à connaître : pendant la coupure du service DNS d'IoBase constatée depuis le 29 août 2026, chaque mesure émise a été définitivement perdue. Le journal local permet de savoir ce qui a été perdu, pas de le rattraper.