Aller au contenu

Arc42 — Section 5 : Décomposition en blocs

5.1 Vue d'ensemble des modules

graph TB
    subgraph Frontend["Frontend (Next.js)"]
        PAGE[app/page.tsx]
        APP[components/app-client.tsx]
        API_CLIENT[lib/api-client.ts]
        TYPES[lib/types.ts]
        REGISTRY[lib/device-registry.ts]
        DATA[lib/data-provider.tsx]
        PIPECTX[lib/pipeline-context.tsx]
        PIPESTG[lib/pipeline-stages.ts]
        GLOSSARY[lib/glossary.ts]
        MOCK[lib/mock-store.ts]
        EXPORT[lib/exporters.ts]
        CONST[lib/constants.ts]
        LORAWAN[lib/lorawan.ts]

        subgraph Components["Components"]
            DASH[components/dashboard/]
            CONV[components/converter/]
            PIPE[components/pipeline/]
            LAYOUT[components/layout/]
            SHARED[components/shared/]
        end
    end

    subgraph Backend["Backend (FastAPI)"]
        MAIN[app/main.py]
        CONFIG[app/config.py]
        DB[app/database.py]
        SEC[app/security.py]
        SECH[app/security_headers.py]
        WS[app/websocket.py]
        AUDIT[app/audit.py]
        CODEC[app/payload_codec.py]
        LOG[app/logging_config.py]
        MQTT_H[app/mqtt_handler.py]
        RL[app/rate_limit.py]
        DBUF[app/debug_buffer.py]
        ERR[app/errors.py]

        subgraph Repositories["Repositories"]
            DEV_REPO[app/repositories/device_repo.py]
            ALERT_REPO[app/repositories/alert_repo.py]
            STATS_REPO[app/repositories/stats_repo.py]
        end

        subgraph Models["Models"]
            ALERT_MODEL[app/models/alert.py]
            DEVICE_MODEL[app/models/device.py]
            MESURE_MODEL[app/models/mesure.py]
        end

        subgraph Services["Services"]
            MQTT_SVC[app/services/mqtt_service.py]
        end

        subgraph Utils["Utils"]
            RETRY[app/utils/retry.py]
        end

        subgraph Routes
            HEALTH[routes/health.py]
            DEVICES[routes/devices.py]
            ALERTS[routes/alerts.py]
            STATS[routes/stats.py]
            DEBUG[routes/debug.py]
        end
    end

    subgraph Workers["Workers"]
        SUB[subscriber.py]
        PUB[publisher.py]
        CODEC_W[app/payload_codec.py]
    end

    APP --> API_CLIENT
    APP --> EXPORT
    API_CLIENT --> MAIN
    MAIN --> Routes
    MAIN --> MQTT_H
    MAIN --> DB
    MAIN --> WS
    Routes --> DB
    Routes --> SEC
    SUB --> DB
    SUB --> CODEC_W
    PUB --> CODEC_W

5.1.1 Vue Conteneurs C4 (Niveau 2)

Le diagramme de conteneurs C4 détaille les 6 services déployés et leurs protocoles de communication.

graph TB
    TECH["👤 Technicien IoT"]

    subgraph Docker["Docker Compose"]
        subgraph Frontend["Frontend"]
            WEB["🌐 Next.js<br/><i>Dashboard React<br/>:3000</i>"]
        end

        subgraph BackendGroup["Backend Python"]
            API["⚡ FastAPI<br/><i>API REST + WebSocket<br/>:8000</i>"]
            SUB["📥 Subscriber<br/><i>MQTT → PostgreSQL<br/>Worker Python</i>"]
            PUB["📤 Publisher<br/><i>Simulateur capteurs<br/>Worker Python</i>"]
        end

        subgraph Infra["Infrastructure"]
            MQ["📨 Mosquitto<br/><i>Broker MQTT<br/>:1883</i>"]
            PG["🗄️ PostgreSQL<br/><i>Base de données<br/>:5432</i>"]
        end

        subgraph ChirpstackProfile["Chirpstack (profil optionnel)"]
            CS["⚙️ Chirpstack v4<br/><i>:8080</i>"]
            REDIS["💾 Redis<br/><i>:6379</i>"]
        end
    end

    TECH -->|"HTTP / WS"| WEB
    WEB -->|"REST JSON<br/>WebSocket"| API
    API -->|"SQL<br/>(psycopg2 pool)"| PG
    SUB -->|"Subscribe<br/>QoS 1"| MQ
    SUB -->|"INSERT"| PG
    SUB -->|"asyncio bridge<br/>broadcast()"| API
    PUB -->|"Publish<br/>QoS 1"| MQ
    CS -->|"Publish"| MQ
    CS -->|"Cache"| REDIS

    style WEB fill:#000,color:#fff
    style API fill:#009688,color:#fff
    style SUB fill:#009688,color:#fff
    style PUB fill:#009688,color:#fff
    style MQ fill:#3C1053,color:#fff
    style PG fill:#336791,color:#fff
    style CS fill:#FF6B3E,color:#fff
    style REDIS fill:#DC382D,color:#fff

Catalogue des conteneurs

Conteneur Technologie Port Rôle
Next.js React 19, TypeScript 3000 Dashboard temps réel + Convertisseur LoRaWAN
FastAPI Python, Uvicorn 8000 API REST, WebSocket, health check
Subscriber Python, paho-mqtt Écoute MQTT, décode, insère en DB
Publisher Python, paho-mqtt Simule capteurs Chirpstack v4
Mosquitto Eclipse Mosquitto 2 1883 Broker MQTT publish/subscribe
PostgreSQL PostgreSQL 15 5432 Stockage mesures, alertes
Chirpstack Chirpstack v4 8080 Serveur réseau LoRaWAN (optionnel)
Redis Redis 7 6379 Cache Chirpstack (optionnel)

5.2 Modules Frontend

Module Chemin Responsabilité
page.tsx app/page.tsx Point d'entrée Next.js — rendu côté serveur minimal, délègue tout au composant client
app-client.tsx components/app-client.tsx Composant shell (~19 lignes) — importe NavBar, Dashboard, Converter, Pipeline
types.ts lib/types.ts Types partagés — interfaces TypeScript pour Mesure, Device, Alert, Message WebSocket
device-registry.ts lib/device-registry.ts Source unique IDs/noms capteurs — liste centralisée des appareils et de leurs identificateurs
data-provider.tsx lib/data-provider.tsx React Context Mock/API + WebSocket — fournit les données, gère la connexion, fallback polling
pipeline-context.tsx lib/pipeline-context.tsx React Context Pipeline — gère modes (live/step-by-step/inspector), messages, étape active
pipeline-stages.ts lib/pipeline-stages.ts 8 définitions d'étapes du pipeline avec description, code, couleur
glossary.ts lib/glossary.ts 15 entrées de glossaire IoT/LoRaWAN avec définitions et termes reliés
mock-store.ts lib/mock-store.ts Store simulé avec vrais payloads LoRaWAN — données de développement pour les tests sans backend
api-client.ts lib/api-client.ts Couche d'accès API — toutes les fonctions fetch vers le backend FastAPI et la gestion WebSocket ; isole les détails HTTP du reste du frontend
exporters.ts lib/exporters.ts Logique d'export — génération du fichier CSV (Blob + download) et du PDF (via l'API Canvas/print ou une bibliothèque)
constants.ts lib/constants.ts Constantes partagées — URLs de l'API, seuils d'alerte, durées de polling, labels d'affichage
lorawan.ts lib/lorawan.ts Utilitaires LoRaWAN côté client — fonctions de décodage/formatage des données pour l'affichage (miroir TypeScript du décodage Python)
dashboard/ components/dashboard/ Dashboard, StatsCards, DeviceSelector, MetricsChart, AlertsPanel, ConnectionStatus
converter/ components/converter/ Converter, EncodingPipeline, PipelineStep, DecoderTool, BitManipulator, CorruptionDemo, ProtocolOverhead, NegativeTemperatureDemo
pipeline/ components/pipeline/ Pipeline, PipelineModeTabs, SystemDiagram, DiagramNode, DataPacketAnimation, StageDetailPanel, MessageTimeline, ProtocolInspector, DataTransformPanel, StepByStepControls, StepExplanation
layout/ components/layout/ NavBar, DataModeToggle, NavBtn
shared/ components/shared/ ToastContainer, WsIndicator, HealthIndicator, Skeleton, SectionTitle, Arrow, Row, BinaryDisplay, ConnectionStatus, Term

Détail : app-client.tsx

Après la refactorisation Phase 1, c'est un composant shell très léger (~19 lignes) :

export default function App() {
  const [view, setView] = useState<View>("dashboard")
  return (
    <div className="min-h-screen bg-gray-950 text-white font-mono">
      <ToastContainer />
      <NavBar view={view} setView={setView} />
      {view === "dashboard" && <Dashboard />}
      {view === "converter" && <Converter />}
      {view === "pipeline" && <Pipeline />}
    </div>
  )
}

Détail : api-client.ts

Ce module suit le pattern Repository : il expose des fonctions nommées par domaine métier, pas des appels fetch bruts. Exemple :

export async function fetchHistory(deviceId: string, limit = 20): Promise<Mesure[]> {
  const data = await apiFetch<{ mesures: Mesure[] }>(
    `/devices/${deviceId}/metrics?limit=${limit}`
  )
  return data.mesures.reverse()
}

5.3 Modules Backend

Module Chemin Responsabilité
main.py app/main.py Application FastAPI — point d'entrée Uvicorn, enregistrement des routeurs, middleware CORS, gestion du cycle de vie (startup/shutdown), connexion au broker MQTT au démarrage, WebSocket /ws
config.py app/config.py Configuration centralisée — lecture des variables d'environnement via os.environ.get() ; constantes importées directement
database.py app/database.py Gestion des connexions PostgreSQL — pool de connexions SimpleConnectionPool, context manager get_conn(), utilitaires de requête
security.py app/security.py Authentification par clé API — dependency FastAPI verify_api_key(), comparaison timing-safe avec hmac.compare_digest, logging des échecs
websocket.py app/websocket.py ConnectionManager — gère les connexions WebSocket actives, broadcast avec cap configurable (MAX_WS_CONNECTIONS)
audit.py app/audit.py Middleware ASGI — log chaque requête HTTP (méthode, path, status, durée)
payload_codec.py app/payload_codec.py Codec LoRaWAN unifié — encode/decode binary payloads, extraction Chirpstack v4, validation device IDs
mqtt_handler.py app/mqtt_handler.py Client MQTT FastAPI — gestion de la connexion Mosquitto depuis le contexte de l'application, stockage de la référence à la boucle asyncio pour le bridge WebSocket
rate_limit.py app/rate_limit.py Instance slowapi.Limiter partagée — importée par les routeurs qui appliquent un décorateur @limiter.limit()
logging_config.py app/logging_config.py Configuration du logging — initialise le logger racine avec logging.basicConfig(), format structuré texte
debug_buffer.py app/debug_buffer.py Buffer circulaire thread-safe (deque maxlen=50) pour les messages MQTT de debug
errors.py app/errors.py Hiérarchie d'erreurs personnalisées — AppError, NotFoundError, ValidationError + gestionnaire d'exceptions global
security_headers.py app/security_headers.py Middleware des en-têtes de sécurité HTTP — X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security
device_repo.py app/repositories/device_repo.py Couche d'accès aux données — requêtes centralisées pour les appareils
alert_repo.py app/repositories/alert_repo.py Couche d'accès aux données — requêtes centralisées pour les alertes
stats_repo.py app/repositories/stats_repo.py Couche d'accès aux données — requêtes centralisées pour les statistiques
alert.py app/models/alert.py Modèles de domaine — AlertType enum, structure de données Alerte
device.py app/models/device.py Modèles de domaine — structure de données Appareil
mesure.py app/models/mesure.py Modèles de domaine — structure de données Mesure
mqtt_service.py app/services/mqtt_service.py Logique métier — validation des capteurs, orchestration MQTT
retry.py app/utils/retry.py Utilitaires partagés — backoff exponentiel, décorateur de retry

Détail : security.py

from fastapi import HTTPException, Security
from fastapi.security import APIKeyHeader

api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)

async def verify_api_key(api_key: str | None = Security(api_key_header)):
    if not API_KEY:
        return
    if api_key is None or not hmac.compare_digest(api_key, API_KEY):
        raise HTTPException(status_code=401, detail="Invalid or missing API key")

Détail : database.py

@contextmanager
def get_conn():
    pool = get_pool()
    conn = pool.getconn()
    try:
        yield conn
    finally:
        pool.putconn(conn)

5.3.1 Vue Composants C4 (Niveau 3 — FastAPI)

La vue composants C4 détaille l'architecture interne du conteneur FastAPI.

graph TB
    subgraph FastAPI["FastAPI (app/)"]
        MAIN["main.py<br/><i>Entrypoint, lifespan,<br/>middleware, WS endpoint</i>"]

        subgraph Middleware["Middleware"]
            AUDIT["audit.py<br/><i>Log chaque requête</i>"]
            CORS["CORSMiddleware<br/><i>Origins autorisés</i>"]
            RATE["SlowAPIMiddleware<br/><i>Rate limiting</i>"]
        end

        subgraph Routes["Routes"]
            HEALTH["health.py<br/><i>GET / , GET /health</i>"]
            DEVICES["devices.py<br/><i>GET /devices<br/>GET /devices/{id}/metrics</i>"]
            ALERTS["alerts.py<br/><i>GET /alerts</i>"]
            STATS["stats.py<br/><i>GET /stats</i>"]
        end

        subgraph Core["Core"]
            CONFIG["config.py<br/><i>Variables d'environnement</i>"]
            DB["database.py<br/><i>Pool psycopg2<br/>get_conn()</i>"]
            SEC["security.py<br/><i>verify_api_key()<br/>hmac timing-safe</i>"]
            WS["websocket.py<br/><i>ConnectionManager<br/>broadcast()</i>"]
            MQTT["mqtt_handler.py<br/><i>Client MQTT interne<br/>bridge asyncio</i>"]
            CODEC["payload_codec.py<br/><i>Encode/decode<br/>LoRaWAN binaire</i>"]
            LOG["logging_config.py<br/><i>Format structuré</i>"]
            RL["rate_limit.py<br/><i>Limiter instance</i>"]
        end
    end

    MAIN --> Routes
    MAIN --> Middleware
    MAIN --> WS
    MAIN --> MQTT
    Routes --> DB
    Routes --> SEC
    Routes --> RL
    MQTT --> WS
    MQTT --> CODEC

    style MAIN fill:#009688,color:#fff
    style DB fill:#336791,color:#fff
    style WS fill:#4CAF50,color:#fff
    style MQTT fill:#3C1053,color:#fff
    style SEC fill:#F44336,color:#fff
    style CODEC fill:#FF9800,color:#fff
Composant Responsabilité principale
main.py Point d'entrée Uvicorn, enregistre les routeurs, configure le cycle de vie (MQTT au démarrage), définit l'endpoint WebSocket /ws
config.py Source unique de configuration via os.environ.get() — DB, MQTT, alertes, sécurité, limites
database.py Pool de connexions SimpleConnectionPool (min 2, max 10), context manager get_conn()
security.py Dependency FastAPI verify_api_key() — comparaison timing-safe hmac.compare_digest, retourne 401
websocket.py ConnectionManager — cap configurable (MAX_WS_CONNECTIONS), broadcast thread-safe avec verrou asyncio
mqtt_handler.py Client paho-mqtt dans un thread dédié, valide les plages physiques, bridge vers la boucle asyncio
payload_codec.py Codec unifié — encode_payload(), decode_payload(), decode_chirpstack_payload(), validate_device_id()
audit.py Middleware ASGI — log méthode, path, status code et durée pour chaque requête

5.3.2 Diagramme de classes UML (Backend)

classDiagram
    class ConnectionManager {
        -list~WebSocket~ active_connections
        -int _max_connections
        -asyncio.Lock _lock
        +connect(websocket) bool
        +disconnect(websocket) void
        +broadcast(message) void
    }

    class AuditMiddleware {
        +dispatch(request, call_next) Response
    }

    class AppError {
        -str message
        -int status_code
        +AppError(message, status_code)
    }

    class NotFoundError {
        +NotFoundError(resource)
    }

    class ValidationError {
        +ValidationError(message)
    }

    class SecurityHeadersMiddleware {
        +dispatch(request, call_next) Response
    }

    class SimpleConnectionPool {
        -int minconn
        -int maxconn
        +getconn() connection
        +putconn(conn) void
        +closeall() void
    }

    class Config {
        <<module>>
        +str APP_VERSION
        +str DB_HOST
        +int DB_PORT
        +str MQTT_HOST
        +int MQTT_PORT
        +str MQTT_TOPIC
        +str API_KEY
        +float ALERT_TEMP_THRESHOLD
        +int MAX_WS_CONNECTIONS
        +list DEVICE_EUIS
        +int PUBLISH_INTERVAL
    }

    class PayloadCodec {
        <<module>>
        +encode_payload(temp, hum) str
        +decode_payload(data_b64) dict
        +decode_chirpstack_payload(payload) dict
        +extract_device_id(payload) str
        +validate_device_id(device_id) bool
    }

    class Security {
        <<module>>
        +verify_api_key(api_key) void
    }

    class HealthRouter {
        <<router>>
        +root() dict
        +health_check() dict
    }

    class DevicesRouter {
        <<router>>
        +list_devices() dict
        +device_metrics(device_id, limit) dict
    }

    class AlertsRouter {
        <<router>>
        +get_alerts() dict
    }

    class StatsRouter {
        <<router>>
        +global_stats() dict
    }

    NotFoundError --|> AppError
    ValidationError --|> AppError

    HealthRouter --> SimpleConnectionPool : get_conn()
    DevicesRouter --> SimpleConnectionPool : get_conn()
    DevicesRouter --> Security : verify_api_key
    DevicesRouter --> PayloadCodec : validate_device_id
    AlertsRouter --> SimpleConnectionPool : get_conn()
    AlertsRouter --> Security : verify_api_key
    StatsRouter --> SimpleConnectionPool : get_conn()
    StatsRouter --> Security : verify_api_key
    ConnectionManager --> Config : MAX_WS_CONNECTIONS

5.4 Modules Routes

Module Chemin Endpoints Responsabilité
health.py routes/health.py GET /health Vérifie la connexion à PostgreSQL avec SELECT 1 ; renvoie le statut global du service
devices.py routes/devices.py GET /devices, GET /devices/{id}/metrics CRUD lecture des appareils et consultation des mesures avec pagination
alerts.py routes/alerts.py GET /alerts, GET /alerts/active Consultation des alertes générées, filtrage par statut et par appareil
stats.py routes/stats.py GET /stats Statistiques agrégées (nb_devices, total_mesures, temp_moyenne_globale, derniere_activite)
debug.py routes/debug.py GET /debug/recent-messages, GET /status Endpoints de diagnostic et de statut système détaillé

Note : Le WebSocket /ws est défini dans app/main.py et utilise la classe ConnectionManager de app/websocket.py.


5.5 Workers

Module Chemin Responsabilité
subscriber.py backend/subscriber.py Processus autonome — s'abonne au topic MQTT application/+/device/+/event/up, décode les payloads Chirpstack v4, insère en base, notifie FastAPI
publisher.py backend/publisher.py Processus autonome de simulation — génère des mesures réalistes, les encode en binaire + base64, publie sur MQTT toutes les N secondes, utilise DEVICE_EUIS de la config
payload_codec.py app/payload_codec.py Codec partagé — encode/decode les payloads binaires LoRaWAN, utilisé par publisher et subscriber

Détail : subscriber.py — bridge asyncio

La difficulté principale du subscriber est de notifier FastAPI (qui tourne dans une boucle asyncio) depuis un thread MQTT (synchrone). La solution utilise la classe ConnectionManager de app/websocket.py :

import asyncio
from app.websocket import manager

# Référence à la boucle asyncio de FastAPI, passée au subscriber au démarrage
_loop: asyncio.AbstractEventLoop = None

def on_message(client, userdata, msg):
    """Callback MQTT — s'exécute dans le thread paho-mqtt, pas dans asyncio."""
    mesure = decode_payload(msg.payload)
    insert_mesure(mesure)  # psycopg2 — synchrone, OK

    # Bridge thread → asyncio
    if _loop and not _loop.is_closed():
        asyncio.run_coroutine_threadsafe(manager.broadcast(mesure), _loop)

5.6 Dépendances inter-modules

Module source Module cible Nature de la dépendance
app-client.tsx api-client.ts Import TypeScript — toutes les requêtes HTTP/WS passent par ce module
app-client.tsx exporters.ts Import TypeScript — appel lors du clic sur "Exporter CSV/PDF"
pipeline-context.tsx pipeline-stages.ts Import TypeScript — charge les définitions des 8 étapes
pipeline-context.tsx data-provider.tsx Import TypeScript — accède aux messages WebSocket temps réel
main.py config.py Import Python — lecture des settings au démarrage
main.py database.py Import Python — initialisation du pool de connexions
main.py mqtt_handler.py Import Python — connexion au broker MQTT au démarrage (on_startup)
routes/*.py database.py Import Python — chaque route utilise get_conn()
routes/*.py security.py Import Python — dependency injection FastAPI dans les routes protégées
routes/*.py rate_limit.py Import Python — décorateur @limiter.limit() sur les endpoints publics
debug.py debug_buffer.py Import Python — expose le buffer de debug via l'API
subscriber.py database.py Import Python — INSERT des mesures
mqtt_handler.py app/websocket.py Import Python — appel à broadcast() via bridge asyncio
mqtt_handler.py debug_buffer.py Import Python — écrit les messages MQTT bruts dans le buffer

5.7 Modèle Logique de Données (MERISE MLD)

Le MLD traduit le MCD en structure relationnelle avec les clés primaires, clés étrangères et types de données.

Schéma relationnel

mesures (
    id          SERIAL       PRIMARY KEY,
    device_id   VARCHAR(64)  NOT NULL,
    temperature NUMERIC(5,2),
    humidite    NUMERIC(5,2),
    recu_le     TIMESTAMP    DEFAULT NOW()
)
erDiagram
    mesures {
        int id PK "SERIAL"
        varchar device_id "VARCHAR(64) NOT NULL"
        numeric temperature "NUMERIC(5,2)"
        numeric humidite "NUMERIC(5,2)"
        timestamp recu_le "DEFAULT NOW()"
    }

Règles de passage MCD → MLD

Règle Application
Entité → Table MESURE → mesures
Identifiant → Clé primaire id SERIAL PRIMARY KEY
Association 1,N device_id comme attribut dans mesures (pas de table CAPTEUR séparée)
Attributs → Colonnes Typage adapté à PostgreSQL

Dépendances fonctionnelles

id → device_id, temperature, humidite, recu_le
  • La table est en 3ème Forme Normale (3FN) : chaque attribut non-clé dépend uniquement de la clé primaire.
  • device_id n'est pas une clé étrangère vers une table capteurs (choix de simplification documenté dans le MCD).

Index

Nom Colonnes Justification
idx_mesures_device_id device_id Filtrage par capteur (GET /devices/{id}/metrics)
idx_mesures_recu_le recu_le DESC Requêtes time-series, tri chronologique
idx_mesures_device_time device_id, recu_le DESC Index composite pour les requêtes historiques par capteur