Aller au contenu

Architecture

L'architecture interne de Miravo — moteur, graphe d'actifs, bus d'événements, scheduler de ticks, pipeline de générateurs, compilateur de modèles et interface adaptateur.

Miravo suit un flux de données unidirectionnel : la configuration entre, la sortie protocole sort.

Config (.twin.yaml + .miravo.yaml)
-> Twin Runtime (compiler les modèles, instancier les actifs)
-> Tick Scheduler (boucle à intervalle fixe)
-> Generators + Lifecycle + Faults (évaluer les membres)
-> Asset Graph (source de vérité)
-> Event Bus (événements typés)
-> Protocol Adapters (MQTT, OPC UA, and more)

La factory createEngine() assemble tout et retourne l’API publique : start, stop, exécuter des commandes, enregistrer des adaptateurs, obtenir métriques/état. Le moteur gère le cycle de vie de tous les sous-systèmes.

Enregistre les modèles compilés et instancie les actifs. Chaque instance reçoit :

  • Un RNG forké (aléatoire déterministe par instance)
  • Des paramètres variés (basé sur le paramètre variation)
  • Des instances de générateurs par instance (les générateurs avec état comme random-walk sont indépendants)
  • Des overrides de membres accessibles en écriture (pour les méthodes comme SetSpeed)

Exécute la boucle de tick à un intervalle fixe (défaut 1000ms). Chaque tick :

  1. Snapshot des paramètres de chaque instance (vue cohérente au tick)
  2. Évaluation de tous les membres dans l’ordre de déclaration
  3. Application des effets de cycle de vie (valeur * multiplicateur + offset)
  4. Application des effets de défaillance (spike, puis multiplicateur, puis offset)
  5. Écriture des valeurs dans le graphe d’actifs (ignorer les valeurs inchangées)
  6. Émission de tick:complete avec le snapshot et le delta du graphe

Le scheduler utilise writeMemberValue() pour ignorer les écritures quand la valeur et la qualité sont inchangées, préservant les timestamps et évitant les entrées delta inutiles.

Le AssetGraph est la source de vérité à l’exécution. Il contient toutes les instances AssetNode actives avec leurs valeurs de membres actuelles, paramètres, état de cycle de vie et défaillances actives. Les snapshots sont immuables par contrat — les adaptateurs de protocole reçoivent une vue cohérente à un instant précis.

Un bus mitt typé transporte toute communication interne. Les composants ne se référencent jamais directement. Événements clés :

ÉvénementPayload
tick:complete{ graph: AssetGraphSnapshot, delta: AssetGraphDelta }
instance:created / instance:removed{ id }
lifecycle:changed{ instanceId, stage }
fault:triggered / fault:cleared{ instanceId, fault }
engine:state-changed{ from, to } (idle, running, paused, stopped)
adapter:enabled / adapter:disabled{ name, endpoint? }

compileModel() valide une définition de modèle brute :

  • Ordonnancement topologique des dépendances entre membres
  • Validation des références $param.X (le paramètre doit exister et être numérique)
  • Références input.member (doit être déclaré plus tôt)
  • Types d’arguments de méthode et cibles accessibles en écriture
  • Cibles d’effets de cycle de vie/défaillance

Les modèles invalides échouent à la compilation, avant que la simulation ne démarre.

Tous les adaptateurs de protocole implémentent l’interface ProtocolAdapter :

interface ProtocolAdapter {
name: string;
start(config: unknown): Promise<void>;
stop(): Promise<void>;
onTick(graph: AssetGraphSnapshot): void;
getMetrics(): AdapterMetrics;
}

Les adaptateurs reçoivent le snapshot du graphe à chaque tick. Ils ne calculent jamais les valeurs des membres — ils projettent uniquement ce que le moteur fournit. Cette interface est celle utilisée par MQTT et OPC UA aujourd’hui, et celle par laquelle les futurs protocoles (Modbus TCP, Sparkplug B et d’autres) seront ajoutés.

Tous les adaptateurs actuels utilisent des mises à jour pilotées par delta. onTick() reçoit le snapshot complet du graphe à chaque tick. Les changements structurels (création et suppression d’instances) sont traités au fur et à mesure pour que l’espace d’adresses reste cohérent entre les ticks.

Miravo utilise un PRNG xoshiro128** avec graine. Chaque instance reçoit un RNG forké depuis la graine parent. Même graine + même config = sortie bit-identique. Math.random() et Date.now() ne sont jamais utilisés.

Le ContentCatalog résout les modèles et templates à travers trois couches :

  1. Répertoire de travail courant
  2. Registre local (~/.miravo/registry/local/)
  3. Contenu intégré (package @miravo/content)

Les couches de priorité supérieure masquent les couches inférieures par nom.

La persistance d’état sauvegarde les snapshots structurels dans $MIRAVO_HOME/state/<nom>.json. Au démarrage, le moteur restaure depuis le snapshot s’il en existe un. Les sauvegardes sont anti-rebondies et sérialisées pour éviter les conflits d’écriture.