← Blog

Connecter un automate Siemens S7 ou Schneider M340 à un pipeline IA : mode opératoire complet

Mode opératoire pas-à-pas pour lire les données d'un Siemens S7-1500 (snap7/OPC-UA) et d'un Schneider M340 (Modbus TCP).

Ce que fait (et ne fait pas) cet article

Cet article explique comment lire les donnees d’un automate industriel — Siemens S7-1500 ou Schneider Modicon M340 — pour les injecter dans un pipeline de traitement de donnees ou d’IA. Il s’agit exclusivement de lecture. A aucun moment nous n’ecrivons dans l’automate depuis le pipeline IA. La raison est a la fois technique et reglementaire, et on y revient en fin d’article.

Le contexte typique : une ETI industrielle qui veut collecter les variables process de ses machines (temperatures, debits, pressions, compteurs) pour alimenter un modele de maintenance predictive, un dashboard de suivi de performance, ou une boucle de controle qualite statistique. Les donnees existent deja dans l’automate. Il suffit de les lire.

Trois methodes sont couvertes :

  1. Siemens S7-1500 via snap7 — acces direct au DB, rapide a mettre en place, limites en securite
  2. Siemens S7-1500 via OPC-UA — methode recommandee, securisee, avec souscription temps reel
  3. Schneider M340 via Modbus TCP — protocole universel, simple mais verbeux

1. Siemens S7-1500 via snap7 (Python)

snap7 est une bibliotheque open source qui implemente le protocole S7comm, le protocole natif de communication des automates Siemens. Elle permet de lire directement les blocs de donnees (DB) d’un S7-1500 depuis n’importe quel langage supportant une FFI C — en pratique, Python via python-snap7.

Configuration TIA Portal

Avant de pouvoir lire un DB depuis l’exterieur, il faut configurer l’automate dans TIA Portal. Trois points critiques.

1. Activer la communication PUT/GET

Dans TIA Portal :

  1. Ouvrir les proprietes de la CPU S7-1500
  2. Navigation : Protection & Security > Connection mechanisms
  3. Cocher Permit access with PUT/GET communication from remote partner

Sans cette case, snap7 recevra une erreur 0x00000005 (access denied) a chaque tentative de connexion.

2. Creer un DB accessible

Creer un Data Block (DB) qui contient les variables a exposer. Point critique : desactiver l’acces optimise du DB.

  1. Creer un nouveau DB (ex: DB100 — AI_Data)
  2. Clic droit sur le DB > Properties
  3. Section Attributes : decocher Optimized block access

Si cette option reste cochee, snap7 ne peut pas adresser les variables par offset (le runtime S7 utilise un adressage interne optimise incompatible avec le protocole S7comm).

3. Definir les variables dans le DB

OffsetNomTypeDescription
0.0debit_litres_hREALDebit mesure (L/h)
4.0pression_barREALPression de ligne (bar)
8.0temperature_cREALTemperature produit (°C)
12.0niveau_cuve_pctREALNiveau cuve (%)
16.0compteur_bouteillesDINTCompteur pieces
20.0machine_en_marcheBOOLBit d’etat machine

Compiler et telecharger le programme dans l’automate.

Code Python snap7

# Installation
pip install python-snap7
# Sur Linux, installer aussi la lib C :
sudo apt install -y libsnap7-dev
"""
Lecture d'un DB Siemens S7-1500 via snap7.
DB100 : variables process machine de remplissage.
"""
import snap7
import struct
import time

# --- Configuration ---
PLC_IP = "192.168.1.10"
RACK = 0    # Rack 0 pour S7-1500
SLOT = 1    # Slot 1 pour S7-1500
DB_NUMBER = 100
DB_SIZE = 22  # Octets a lire (offset 0 a 21)

def connect_plc(ip, rack, slot):
    """Connexion au S7-1500."""
    client = snap7.client.Client()
    client.connect(ip, rack, slot)
    if not client.get_connected():
        raise ConnectionError(f"Connexion echouee vers {ip}")
    print(f"Connecte au S7-1500 @ {ip}")
    return client

def read_db(client, db_number, start, size):
    """Lit un bloc de donnees brut depuis un DB."""
    return client.db_read(db_number, start, size)

def parse_real(data, offset):
    """Extrait un REAL (FLOAT32 big-endian) depuis un buffer."""
    return struct.unpack(">f", data[offset:offset+4])[0]

def parse_dint(data, offset):
    """Extrait un DINT (INT32 big-endian) depuis un buffer."""
    return struct.unpack(">i", data[offset:offset+4])[0]

def parse_bool(data, offset_byte, offset_bit):
    """Extrait un BOOL (bit) depuis un buffer."""
    return bool(data[offset_byte] & (1 << offset_bit))

def main():
    plc = connect_plc(PLC_IP, RACK, SLOT)

    try:
        while True:
            raw = read_db(plc, DB_NUMBER, 0, DB_SIZE)

            debit = parse_real(raw, 0)
            pression = parse_real(raw, 4)
            temperature = parse_real(raw, 8)
            niveau = parse_real(raw, 12)
            compteur = parse_dint(raw, 16)
            en_marche = parse_bool(raw, 20, 0)

            print(
                f"Debit: {debit:.1f} L/h | "
                f"Pression: {pression:.2f} bar | "
                f"Temp: {temperature:.1f}°C | "
                f"Niveau: {niveau:.1f}% | "
                f"Compteur: {compteur} | "
                f"Marche: {en_marche}"
            )
            time.sleep(1.0)

    except KeyboardInterrupt:
        print("Arret")
    finally:
        plc.disconnect()

if __name__ == "__main__":
    main()

Types de donnees Siemens et leur decodage

Type S7TailleFormat struct PythonExemple
BOOL1 bitLecture manuelle du bitdata[offset] & (1 << bit)
BYTE1 octet>Bstruct.unpack(">B", data[o:o+1])
INT2 octets>hstruct.unpack(">h", data[o:o+2])
DINT4 octets>istruct.unpack(">i", data[o:o+4])
REAL4 octets>fstruct.unpack(">f", data[o:o+4])
LREAL8 octets>dstruct.unpack(">d", data[o:o+8])
STRING2 + n octetsOctets 0=max, 1=len, 2+=charsdata[o+2:o+2+data[o+1]].decode()

Attention : Siemens utilise le big-endian (byte order >). C’est l’inverse de la plupart des systemes x86. Oublier le > dans struct.unpack est la premiere source de valeurs aberrantes.

Limitations de snap7

  • Pas de securite. Le protocole S7comm n’a ni authentification ni chiffrement. Quiconque a acces au reseau peut lire (et potentiellement ecrire) dans l’automate.
  • Polling uniquement. Il n’y a pas de mecanisme de souscription. Il faut interroger l’automate a intervalle regulier, meme si les valeurs n’ont pas change.
  • Acces optimise impossible. Les DB avec “Optimized block access” sont inaccessibles.
  • Non recommande par Siemens. Siemens pousse OPC-UA comme protocole d’acces standard. snap7 utilise un protocole reverse-engineered.

Pour un POC ou un pilote, snap7 suffit. Pour la production, passez a OPC-UA.


2. Siemens S7-1500 via OPC-UA (methode recommandee)

OPC-UA (Open Platform Communications - Unified Architecture) est le protocole standard d’echange de donnees industrielles. Depuis le firmware V2.0, tous les S7-1500 integrent un serveur OPC-UA natif. C’est la methode recommandee par Siemens, et pour de bonnes raisons.

Pourquoi OPC-UA plutot que snap7

Criteresnap7OPC-UA
SecuriteAucuneCertificats X.509, chiffrement AES-256
DecouverteManuelle (offsets)Automatique (browse du namespace)
SouscriptionNon (polling)Oui (notification sur changement)
SemantiqueRegistres brutsVariables nommees avec type et unite
Support SiemensNon officielOfficiellement supporte
PerformanceRapide (protocole leger)Legerement plus lent (overhead TLS)

Configuration du serveur OPC-UA dans TIA Portal

  1. Ouvrir les proprietes de la CPU S7-1500
  2. Navigation : OPC UA > Server
  3. Cocher Activate OPC UA Server
  4. Configurer le mode de securite : Sign & Encrypt (recommande) ou None (POC uniquement)
  5. Si securise : generer un certificat serveur via TIA Portal, exporter le certificat client
  6. Dans le DB a exposer, clic droit > Properties > Attributes > cocher Accessible from HMI/OPC UA
  7. Compiler et telecharger

Port par defaut. Le serveur OPC-UA du S7-1500 ecoute sur le port 4840. L’URL endpoint est opc.tcp://{ip}:4840. Assurez-vous que le pare-feu reseau autorise ce port entre le device edge et l’automate.

Code Python asyncua

# Installation
pip install asyncua
"""
Lecture d'un S7-1500 via OPC-UA avec souscription.
Le modele reçoit les mises a jour en temps reel (notification sur changement).
"""
import asyncio
from asyncua import Client, ua

# --- Configuration ---
OPC_UA_URL = "opc.tcp://192.168.1.10:4840"
# En mode securise, ajouter :
# CERT_PATH = "/home/nika/certs/client_cert.der"
# KEY_PATH = "/home/nika/certs/client_key.pem"

# NodeIDs des variables (format : ns=3;s="DB100"."debit_litres_h")
# Trouvez-les avec un browse (voir ci-dessous)
NODES = {
    "debit": 'ns=3;s="DB100"."debit_litres_h"',
    "pression": 'ns=3;s="DB100"."pression_bar"',
    "temperature": 'ns=3;s="DB100"."temperature_c"',
    "niveau": 'ns=3;s="DB100"."niveau_cuve_pct"',
    "compteur": 'ns=3;s="DB100"."compteur_bouteilles"',
    "en_marche": 'ns=3;s="DB100"."machine_en_marche"',
}

class DataChangeHandler:
    """Callback appele a chaque changement de valeur."""
    def datachange_notification(self, node, val, data):
        print(f"  >> {node} = {val}")

async def browse_namespace(client):
    """Parcourt le namespace OPC-UA pour decouvrir les variables disponibles."""
    root = client.get_root_node()
    objects = await root.get_child(["0:Objects"])
    print("Namespace OPC-UA :")
    for child in await objects.get_children():
        name = await child.read_browse_name()
        print(f"  {name.Name} (NodeId: {child.nodeid})")
        # Descendre d'un niveau pour voir les variables
        try:
            for sub in await child.get_children():
                sub_name = await sub.read_browse_name()
                print(f"    {sub_name.Name} (NodeId: {sub.nodeid})")
        except Exception:
            pass

async def read_once(client, nodes):
    """Lecture ponctuelle de toutes les variables."""
    print("\n--- Lecture ponctuelle ---")
    for label, node_id in nodes.items():
        node = client.get_node(node_id)
        value = await node.read_value()
        print(f"  {label}: {value}")

async def subscribe_changes(client, nodes):
    """Souscription aux changements de valeur (notification temps reel)."""
    handler = DataChangeHandler()
    subscription = await client.create_subscription(500, handler)  # 500 ms interval

    node_objects = [client.get_node(nid) for nid in nodes.values()]
    await subscription.subscribe_data_change(node_objects)

    print("\nSouscription active. En attente de changements...")
    # Maintenir la souscription active
    while True:
        await asyncio.sleep(1)

async def main():
    async with Client(url=OPC_UA_URL) as client:
        # Mode securise (decommenter si certificats configures) :
        # await client.set_security_string(
        #     f"Basic256Sha256,SignAndEncrypt,{CERT_PATH},{KEY_PATH}"
        # )

        print(f"Connecte au serveur OPC-UA @ {OPC_UA_URL}")

        # 1. Browse pour decouvrir les variables
        await browse_namespace(client)

        # 2. Lecture ponctuelle
        await read_once(client, NODES)

        # 3. Souscription temps reel
        await subscribe_changes(client, NODES)

if __name__ == "__main__":
    asyncio.run(main())

Avantages en production

  • Souscription : le client recoit une notification uniquement quand la valeur change. Pas de polling inutile. Reduction de la charge reseau de 80 a 95 %.
  • Browse : pas besoin de connaitre les offsets memoire a l’avance. Le client parcourt le namespace et decouvre les variables avec leurs noms, types et unites.
  • Securite : le chiffrement TLS empeche le sniffing sur le reseau OT. L’authentification par certificat empeche les connexions non autorisees.
  • Interoperabilite : OPC-UA est constructeur-agnostique. Le meme code Python fonctionne avec un Siemens S7-1500, un Beckhoff TwinCAT, un Rockwell ControlLogix (via CIP-to-OPCUA bridge), ou un Schneider M580.

3. Schneider M340 via Modbus TCP

Le Modicon M340 de Schneider Electric est un automate milieu de gamme tres repandu dans l’industrie francaise, notamment dans le traitement de l’eau, l’agroalimentaire et la chimie. Sa methode d’acces native pour la lecture de donnees est Modbus TCP.

Configuration Unity Pro / EcoStruxure Control Expert

Unity Pro (renomme EcoStruxure Control Expert dans les versions recentes) est l’atelier de programmation des automates Schneider. La configuration Modbus TCP se fait en deux etapes.

1. Verifier que le module Ethernet est configure

  1. Dans l’arborescence du projet, ouvrir Configuration > Bus PLC
  2. Verifier que le module BMX NOC 0401 (ou BMX NOE 0110) est present
  3. Double-clic sur le module > onglet IP Configuration
  4. Noter l’adresse IP de l’automate (ex: 192.168.1.20)
  5. Le serveur Modbus TCP est actif par defaut sur le port 502

2. Mapper les variables sur des registres Modbus

Dans Schneider, les registres Modbus sont mappes sur les variables de l’application via le concept d’adresses directes (%MW, %MD, %M).

Adresse directeRegistre Modbus (holding)TypeDescription
%MW10040101INT16Debit (x10, en L/h)
%MW10140102INT16Pression (x100, en bar)
%MD10240103-40104FLOAT32Temperature (°C)
%MD10440105-40106FLOAT32Niveau cuve (%)
%MD10640107-40108DINT32Compteur bouteilles
%M200Coil 200BOOLMachine en marche

Convention d’adressage Schneider. L’adresse Modbus du registre %MWn est n + 1 dans la convention 1-based (standard Modbus), ou n dans la convention 0-based (utilisee par pymodbus). Le code ci-dessous utilise la convention 0-based : %MW100 se lit a l’adresse 100.

Point critique : les FLOAT32 (REAL) occupent 2 registres Modbus consecutifs (32 bits = 2 x 16 bits). L’ordre des mots (word order) est Big-Endian chez Schneider par defaut : le mot de poids fort est dans le registre d’adresse inferieure.

3. Assigner les variables du programme aux adresses directes

Dans le programme automate (Structured Text ou Ladder), assigner les variables process aux adresses directes :

(* Structured Text — Unity Pro *)
%MW100 := INT_TO_WORD(REAL_TO_INT(debit_mesure * 10.0));
%MW101 := INT_TO_WORD(REAL_TO_INT(pression_mesure * 100.0));
%MD102 := REAL_TO_DWORD(temperature_mesure);
%MD104 := REAL_TO_DWORD(niveau_cuve);
%MD106 := DINT_TO_DWORD(compteur_bouteilles);
%M200  := machine_en_marche;

Code Python pymodbus

# Installation
pip install pymodbus==3.6.9
"""
Lecture d'un Schneider M340 via Modbus TCP.
Variables process d'une machine de remplissage.
"""
import struct
import time
from pymodbus.client import ModbusTcpClient
from pymodbus.constants import Endian
from pymodbus.payload import BinaryPayloadDecoder

# --- Configuration ---
PLC_IP = "192.168.1.20"
PLC_PORT = 502
UNIT_ID = 1  # Slave ID (generalement 1 pour Modbus TCP)

def connect_plc(ip, port):
    """Connexion Modbus TCP."""
    client = ModbusTcpClient(ip, port=port)
    if not client.connect():
        raise ConnectionError(f"Connexion echouee vers {ip}:{port}")
    print(f"Connecte au M340 @ {ip}")
    return client

def read_int16_scaled(client, address, scale, unit=1):
    """Lit un INT16 et applique un facteur d'echelle."""
    result = client.read_holding_registers(address, count=1, slave=unit)
    if result.isError():
        raise IOError(f"Erreur lecture registre {address}: {result}")
    raw = result.registers[0]
    # Gestion du signe (INT16 signe)
    if raw > 32767:
        raw -= 65536
    return raw / scale

def read_float32(client, address, unit=1):
    """Lit un FLOAT32 sur 2 registres (big-endian word order)."""
    result = client.read_holding_registers(address, count=2, slave=unit)
    if result.isError():
        raise IOError(f"Erreur lecture registres {address}-{address+1}: {result}")
    decoder = BinaryPayloadDecoder.fromRegisters(
        result.registers,
        byteorder=Endian.BIG,
        wordorder=Endian.BIG
    )
    return decoder.decode_32bit_float()

def read_dint32(client, address, unit=1):
    """Lit un DINT32 sur 2 registres (big-endian word order)."""
    result = client.read_holding_registers(address, count=2, slave=unit)
    if result.isError():
        raise IOError(f"Erreur lecture registres {address}-{address+1}: {result}")
    decoder = BinaryPayloadDecoder.fromRegisters(
        result.registers,
        byteorder=Endian.BIG,
        wordorder=Endian.BIG
    )
    return decoder.decode_32bit_int()

def read_coil(client, address, unit=1):
    """Lit un BOOL (coil)."""
    result = client.read_coils(address, count=1, slave=unit)
    if result.isError():
        raise IOError(f"Erreur lecture coil {address}: {result}")
    return result.bits[0]

def main():
    client = connect_plc(PLC_IP, PLC_PORT)

    try:
        while True:
            debit = read_int16_scaled(client, 100, scale=10)
            pression = read_int16_scaled(client, 101, scale=100)
            temperature = read_float32(client, 102)
            niveau = read_float32(client, 104)
            compteur = read_dint32(client, 106)
            en_marche = read_coil(client, 200)

            print(
                f"Debit: {debit:.1f} L/h | "
                f"Pression: {pression:.2f} bar | "
                f"Temp: {temperature:.1f}°C | "
                f"Niveau: {niveau:.1f}% | "
                f"Compteur: {compteur} | "
                f"Marche: {en_marche}"
            )
            time.sleep(1.0)

    except KeyboardInterrupt:
        print("Arret")
    finally:
        client.close()

if __name__ == "__main__":
    main()

Conversion des types : le piege classique

Le piege le plus frequent avec Modbus est la conversion des types flottants. Un FLOAT32 (REAL) est code sur 32 bits, soit 2 registres Modbus de 16 bits. Mais l’ordre des deux mots peut varier selon le constructeur :

ConstructeurWord orderByte order
Schneider (M340, M580)Big-endianBig-endian
Siemens (via passerelle)Big-endianBig-endian
Allen-Bradley (via passerelle)Little-endianBig-endian
WagoLittle-endianBig-endian

Si vos valeurs lues semblent aberrantes (ex: temperature de 3.7e+18 au lieu de 23.5), le probleme est presque certainement un word order inverse. Changez wordorder=Endian.BIG en wordorder=Endian.LITTLE et relisez.


4. Cas d’usage : machine de remplissage de biere

Le contexte

Une brasserie artisanale exploite une soutireuse 12 becs qui remplit des bouteilles de 33 cL a un rythme de 3 000 bouteilles/heure. La machine est pilotee par un Siemens S7-1500 (soutireuse + bouchage) et un Schneider M340 (convoyeur + pasteurisation). Le probleme : la derive progressive du volume de remplissage. En debut de poste, le volume est a 33.0 cL. En fin de poste, il derive vers 33.8 cL. Sur une journee de production de 24 000 bouteilles, la surconsommation est d’environ 192 litres de biere — soit l’equivalent de 580 bouteilles.

Variables a collecter

VariableSourceProtocoleAdresseTypeUnite
Debit remplissageS7-1500OPC-UA"DB100"."debit_litres_h"REALL/h
Pression CO2S7-1500OPC-UA"DB100"."pression_bar"REALbar
Temperature biereS7-1500OPC-UA"DB100"."temperature_c"REAL°C
Niveau cuve tamponS7-1500OPC-UA"DB100"."niveau_cuve_pct"REAL%
Compteur bouteillesS7-1500OPC-UA"DB100"."compteur_bouteilles"DINTpcs
Temperature pasteuriseurM340Modbus TCP @102%MD102REAL°C
Vitesse convoyeurM340Modbus TCP @100%MW100INT16 (x10)m/min
Pression eau pasteurisationM340Modbus TCP @104%MD104REALbar

Architecture du pipeline

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#1a1a2e', 'primaryTextColor': '#ffffff', 'primaryBorderColor': '#e94560', 'lineColor': '#e94560', 'secondaryColor': '#16213e', 'tertiaryColor': '#0f3460', 'fontSize': '14px'}}}%%
flowchart LR
    subgraph OT["Reseau OT (192.168.1.x)"]
        A["S7-1500\nSoutireuse"] 
        B["M340\nConvoyeur + Pasteur."]
    end

    subgraph EDGE["Edge Device (Jetson Orin Nano)"]
        C["Collecteur\nOPC-UA + Modbus"]
        D["TimescaleDB\nStockage local"]
        E["ML Pipeline\nEWMA + XGBoost"]
    end

    subgraph VIZ["Monitoring"]
        F["Grafana\nDashboard temps reel"]
        G["Alertes\nEmail + WhatsApp"]
    end

    A -->|OPC-UA\nport 4840| C
    B -->|Modbus TCP\nport 502| C
    C --> D
    D --> E
    D --> F
    E -->|anomalie detectee| G

    style A fill:#16213e,stroke:#e94560,color:#ffffff
    style B fill:#16213e,stroke:#e94560,color:#ffffff
    style C fill:#0f3460,stroke:#e94560,color:#ffffff
    style D fill:#0f3460,stroke:#e94560,color:#ffffff
    style E fill:#0f3460,stroke:#e94560,color:#ffffff
    style F fill:#1a1a2e,stroke:#e94560,color:#ffffff
    style G fill:#1a1a2e,stroke:#e94560,color:#ffffff
    style OT fill:#1a1a2e,stroke:#e94560,color:#ffffff
    style EDGE fill:#16213e,stroke:#e94560,color:#ffffff
    style VIZ fill:#0f3460,stroke:#e94560,color:#ffffff

Le modele : EWMA + XGBoost

Le pipeline de detection de derive utilise deux couches :

Couche 1 — EWMA (Exponentially Weighted Moving Average). Un lissage exponentiel avec λ=0.1\lambda = 0.1 sur le debit de remplissage. L’EWMA detecte les drifts lents (derive progressive du volume) en accumulant les ecarts par rapport a la cible. Quand l’EWMA depasse la limite de controle (±3σ\pm 3\sigma), une alerte est levee.

Zt=0.1Xt+0.9Zt1Z_t = 0.1 \cdot X_t + 0.9 \cdot Z_{t-1}

Couche 2 — XGBoost (predictif). Un modele XGBoost entraine sur l’historique de production predit le volume de remplissage en fonction des conditions process (temperature biere, pression CO2, niveau cuve, vitesse convoyeur, duree depuis nettoyage). Le modele identifie les combinaisons de facteurs qui menent a une derive et declenche une alerte preventive avant que la derive ne se materialise.

Features du modele XGBoost :

FeatureSourceTransformation
temperature_biereS7-1500EWMA (lambda=0.2)
pression_co2S7-1500Brut
niveau_cuveS7-1500Gradient (delta/min)
vitesse_convoyeurM340Brut
temp_pasteuriseurM340EWMA (lambda=0.1)
heures_depuis_nettoyageCalculeLineaire
compteur_bouteilles_posteS7-1500Cumul depuis debut poste

Target : volume_reel (mesure par pesee echantillon toutes les 30 min)

Resultats mesures

Apres 3 mois de fonctionnement en production :

IndicateurAvantApresAmelioration
Derive volume en fin de poste+0.8 cL/bouteille+0.1 cL/bouteille-87%
Surconsommation biere/jour192 L24 L-87%
Rebuts volume hors tolerance340 bouteilles/jour48 bouteilles/jour-85%
Surconsommation annuelle (250j)48 000 L6 000 L-87%
Economie annuelle (biere a 3 EUR/L)126 000 EUR
Cout du projet (materiel + integration)8 500 EURROI < 1 mois

Le retour sur investissement est inferieur a un mois. Le principal facteur de gain n’est pas l’IA elle-meme — c’est la visibilite temps reel sur la derive, rendue possible par la connexion automate.

Compensation continue

Le systeme ne corrige pas la consigne de remplissage automatiquement — ce serait de l’ecriture dans l’automate, et c’est interdit par le pipeline IA (voir section suivante). Ce qu’il fait :

  1. Detecte la derive (EWMA depasse le seuil)
  2. Identifie la cause probable (XGBoost feature importance : temperature biere + heures depuis nettoyage)
  3. Alerte l’operateur avec une recommandation : “Debit en derive (+0.3 cL). Cause probable : temperature biere a 5.8°C (cible 4.0°C). Action suggeree : verifier le refroidissement.”
  4. L’operateur ajuste la consigne manuellement dans le SCADA

C’est un systeme d’aide a la decision, pas un systeme de commande.


5. Securite : pourquoi ne JAMAIS ecrire dans l’automate depuis le pipeline IA

Le pipeline IA lit les donnees de l’automate. Il ne les ecrit jamais. Ce n’est pas une limitation technique — c’est une regle de securite fondamentale. Voici pourquoi.

Raison 1 : la norme IEC 62443

La norme IEC 62443 (cybersecurite des systemes industriels) definit des zones de securite et des niveaux d’assurance. Un pipeline IA qui ecrit dans l’automate cree un chemin d’attaque directe du reseau IT vers le systeme de controle industriel. Meme si le code IA est fiable, il suffit d’une compromission du serveur edge pour prendre le controle de la machine.

Raison 2 : la responsabilite juridique

Si un modele ML modifie une consigne de remplissage et que le produit fini est non conforme (volume insuffisant, contamination par surpression), la question de la responsabilite est un champ de mines juridique. Qui est responsable ? L’editeur du modele ? L’integrateur ? L’exploitant ? En 2026, la jurisprudence est inexistante. Evitez d’etre le cas d’ecole.

Raison 3 : le determinisme

Un automate PLC execute un cycle deterministe, garanti par son runtime (Profinet IRT a 250 us). Un modele ML est par nature probabiliste. Inserer une decision probabiliste dans une boucle deterministe, c’est introduire de l’incertitude dans un systeme concu pour ne pas en avoir.

Raison 4 : la validation

Modifier le programme d’un automate ou ses consignes depuis l’exterieur necessite une revalidation du systeme selon les normes de securite fonctionnelle (IEC 61508, IEC 62061, ISO 13849). Cette revalidation coute entre 10 000 et 100 000 euros selon la categorie de securite.

L’exception : l’adaptive control

Il existe des cas legitimes ou un algorithme ajuste une consigne machine. C’est le domaine du controle adaptatif (adaptive process control, APC). Mais dans ce cas :

  • Le modele tourne sur l’automate lui-meme (pas sur un device edge externe)
  • Les limites de consigne sont cablees en dur dans le programme automate (plages de securite)
  • Le systeme de securite (SIF, safety PLC) reste independant et peut couper a tout moment
  • La validation IEC 61508 est effectuee

Un pipeline IA externe qui pousse des valeurs dans un automate sans passer par ces garde-fous n’est pas du controle adaptatif. C’est une faille de securite.


6. Architecture complete : automate vers edge vers cloud

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#1a1a2e', 'primaryTextColor': '#ffffff', 'primaryBorderColor': '#e94560', 'lineColor': '#e94560', 'secondaryColor': '#16213e', 'tertiaryColor': '#0f3460', 'fontSize': '14px'}}}%%
flowchart TB
    subgraph L0["Niveau 0 — Capteurs / Actionneurs"]
        S1["Capteur debit\n4-20 mA"]
        S2["Capteur pression\nIO-Link"]
        S3["Sonde temperature\nPT100"]
        S4["Camera\nUSB 3.0"]
    end

    subgraph L1["Niveau 1 — Automates"]
        PLC1["Siemens S7-1500\nSoutireuse"]
        PLC2["Schneider M340\nConvoyeur"]
    end

    subgraph DMZ["DMZ IT/OT"]
        FW["Pare-feu\nindustriel"]
    end

    subgraph L2["Niveau 2 — Edge + Supervision"]
        EDGE["Jetson Orin Nano\nCollecte + Inference"]
        DB["TimescaleDB\nHistorian local"]
        SCADA["SCADA / HMI\nSupervision"]
    end

    subgraph L3["Niveau 3-5 — IT / Cloud"]
        DASH["Grafana\nDashboard"]
        ML["Serveur ML\nEntrainement"]
        ERP["ERP\nOrdres de fab"]
    end

    S1 --> PLC1
    S2 --> PLC1
    S3 --> PLC2
    S4 --> EDGE

    PLC1 -->|"OPC-UA\n(lecture seule)"| EDGE
    PLC2 -->|"Modbus TCP\n(lecture seule)"| EDGE
    PLC1 --> SCADA
    PLC2 --> SCADA

    EDGE --> DB
    DB --> DASH
    EDGE -->|"MQTT / REST\n(via DMZ)"| FW
    FW --> ML
    FW --> ERP

    style S1 fill:#1a1a2e,stroke:#e94560,color:#ffffff
    style S2 fill:#1a1a2e,stroke:#e94560,color:#ffffff
    style S3 fill:#1a1a2e,stroke:#e94560,color:#ffffff
    style S4 fill:#1a1a2e,stroke:#e94560,color:#ffffff
    style PLC1 fill:#16213e,stroke:#e94560,color:#ffffff
    style PLC2 fill:#16213e,stroke:#e94560,color:#ffffff
    style FW fill:#e94560,stroke:#ffffff,color:#ffffff
    style EDGE fill:#0f3460,stroke:#e94560,color:#ffffff
    style DB fill:#0f3460,stroke:#e94560,color:#ffffff
    style SCADA fill:#0f3460,stroke:#e94560,color:#ffffff
    style DASH fill:#16213e,stroke:#e94560,color:#ffffff
    style ML fill:#16213e,stroke:#e94560,color:#ffffff
    style ERP fill:#16213e,stroke:#e94560,color:#ffffff
    style L0 fill:#1a1a2e,stroke:#e94560,color:#ffffff
    style L1 fill:#16213e,stroke:#e94560,color:#ffffff
    style DMZ fill:#0f3460,stroke:#e94560,color:#ffffff
    style L2 fill:#0f3460,stroke:#e94560,color:#ffffff
    style L3 fill:#1a1a2e,stroke:#e94560,color:#ffffff

Points cles de cette architecture

  1. Separation IT/OT. Un pare-feu industriel (ex: Fortinet FortiGate Rugged, Cisco IE) isole le reseau OT du reseau IT. Le edge device est dans la zone OT. Seuls les resultats agreges traversent la DMZ.

  2. Lecture seule. Les fleches PLC vers Edge sont unidirectionnelles. Aucun flux ne descend de l’edge vers l’automate.

  3. Stockage local. Le TimescaleDB sur le Jetson conserve un historique local (7 a 30 jours selon la capacite du SSD). Si le lien vers le cloud est coupe, la collecte continue.

  4. Entrainement centralise, inference distribuee. Le modele ML est entraine sur un serveur puissant (GPU datacenter) avec l’historique complet. Le modele entraine est deploye sur le edge device (ONNX/TensorRT) pour l’inference temps reel. C’est le pattern le plus repandu en IA industrielle en 2026.

  5. MQTT pour la remontee. Le protocole MQTT (port 8883 avec TLS) est le standard pour la remontee de donnees du edge vers le cloud. Il est leger, fiable (QoS 1/2), et gere les deconnexions proprement (messages en file d’attente).


Ce qu’on n’a pas couvert

  • Rockwell ControlLogix / CompactLogix — EtherNet/IP et CIP, code Python via pycomm3
  • Beckhoff TwinCAT — ADS protocol, code Python via pyads
  • Historian : OSIsoft PI vs InfluxDB vs TimescaleDB — comparatif pour l’archivage long terme
  • Entrainer le modele XGBoost de detection de derive — preparation des features, entrainement, validation croisee, deploiement
  • OPC-UA Pub/Sub — le mode publication/souscription MQTT d’OPC-UA, disponible sur les S7-1500 firmware V3.0+

Chacun de ces sujets fera l’objet d’un article dedie.

Cet article vous a été utile ?

BCUB3 est une petite structure. Si vous pensez à un collègue ou un partenaire qui pourrait en tirer quelque chose, la meilleure manière de nous aider est de partager le lien. Et si vous avez un cas concret à discuter, parlons-en directement.

Prendre un RDV de cadrage