Skip to main content
Glama

PLC AI Diagnostic & Code Agent

CI Python 3.11+ License: MIT

Local AI agent (Ollama + MCP) for PLC diagnostics and SCL code generation via OPC UA and TIA Portal Openness. A two-layer system that reads industrial PLC data and drafts IEC 61131-3 SCL code — 100 % local, no cloud, no API keys.

Industrial AI portfolio project by eLSeR17. Demonstrates the full IT/OT bridge: an OPC UA monitoring layer feeding a Model Context Protocol (MCP) server, and a code-generation layer that produces validated SCL with a mandatory human review gate.

Problem

Industrial plants run on Siemens S7-1500 PLCs that publish live process data over OPC UA. Turning that data into predictive diagnostics and maintainable control code still requires a specialist staring at tag tables and a separate engineer hand-writing SCL. LLM agents could help with both — if they could read the PLC and write valid, reviewable SCL.

Official vendor AI offerings (e.g. Siemens' Eigen Engineering Agent) are cloud-based, come with licensing limits, and their terms prohibit scripted automation — and many plants cannot let process data leave the shop floor at all. The answer is a local, open-source agent: the LLM runs on your own hardware and your data never leaves your network.

Related MCP server: tiacommander-mcp

Solution (two layers)

Layer

Purpose

Status

Layer 1 — Monitoring

OPC UA client reads PLC tags; an MCP server exposes 7 read-only tools (leer_tags, leer_estado, leer_alarmas, historial_sensores, crear_orden_trabajo, generar_scl, validar_scl) to the agent. Works against the bundled deterministic simulator or a real S7-1500 (native OPC UA server, port 4840).

✅ Phases 1–2

Layer 2 — Code generation

A local ReAct agent (Ollama) instructs a deterministic SCL generator: the LLM selects a template, never writes SCL freely. Output is validated syntactically and, on Windows, can be compiled through an optional TIA Portal Openness bridge. Never downloads to a controller without human review.

✅ Phases 3–6

Non-negotiable safety invariant: the agent is read-only on the PLC in Layer 1, and in Layer 2 its output is validated and reviewed by a human before anything reaches a controller. See Security.

Demo

All outputs below are real, produced by scripts/cli.py against the bundled simulator (no hardware, no network, fully reproducible).

1. PLC diagnostics (live tags + status)

$ python3 scripts/cli.py estado --json
{
  "plc": "opc.tcp://127.0.0.1:4840",
  "mode": "SIMULATOR",
  "last_scan_ms": 50.0,
  "uptime_s": 0.0
}

$ python3 scripts/cli.py tags motor.temp_C --json
{
  "tags": [
    {
      "name": "motor.temp_C",
      "value": 62.47086715698242,
      "unit": "°C",
      "timestamp": "2026-09-11T15:55:16.172664+00:00",
      "quality": 1
    }
  ]
}

$ python3 scripts/cli.py alarmas --json
{ "alarms": [] }   # all 10 monitored tags inside their thresholds

2. Historical series (local CSV historian)

$ PYTHONPATH=src python3 -m plc_ai.data_gen --days 3   # generate history CSV
wrote 4320 rows -> data/historical/history.csv

$ python3 scripts/cli.py historico motor.temp_C --json
{
  "series": [
    {
      "tag": "motor.temp_C",
      "points": [
        {"t": "2026-09-09T15:50:00+00:00", "v": 62.47086728988105},
        {"t": "2026-09-09T16:00:00+00:00", "v": 64.58467365158069},
        ...   (432 points total for 3 days)
      ]
    }
  ]
}

3. CMMS work-order lifecycle (JSON + SAP-PM-style CSV)

$ python3 scripts/cli.py crear-ot --maquina motor --prioridad alta \
    --descripcion "Motor 3 temp high" --accion "Check cooling fan" --json
{ "id_ot": "e4077595", "maquina": "motor", "prioridad": "alta",
  "estado": "abierta", "accion": "Check cooling fan" }

$ python3 scripts/cli.py ot list --json
[ { "id_ot": "e4077595", "maquina": "motor", "prioridad": "alta",
    "estado": "cerrada", "resultado": "reparado" },
  { "id_ot": "3add8851", "maquina": "bomba", "prioridad": "media",
    "estado": "abierta" } ]

$ python3 scripts/cli.py ot close 3add8851 --resultado reparado \
    --observaciones "Seal replaced, no leak" --json
{ "id_ot": "3add8851", "estado": "cerrada", "resultado": "reparado" }

$ python3 scripts/cli.py export-csv --output cmms_export.csv
Exported 2 orders to cmms_export.csv
$ cat cmms_export.csv
id_ot,maquina,prioridad,estado,creada_en,cerrada_en,resultado
e4077595,motor,alta,cerrada,2026-09-11T15:46:51,2026-09-11T15:46:57,reparado
3add8851,bomba,media,cerrada,2026-09-11T15:46:52,2026-09-11T16:03:17,reparado

4. SCL generation + validation

$ python3 scripts/cli.py scl generador --plantilla proteccion_termica \
    --objetivo "Protect conveyor motor from overheating" \
    --parametros '{"limite": 85, "histeresis": 3}' --json
{
  "scl": "// ------------------------------------------------------------------\n// FB_ProteccionTermica - thermal protection with hysteresis\n// GENERATED by plc-ai-agent (template). Review before download.\n// Objetivo: Protect conveyor motor from overheating\n// parametros: limite=85, histeresis=3\n// ------------------------------------------------------------------\nFUNCTION_BLOCK FB_ProteccionTermica\n\nVAR_INPUT\n    temp_actual : REAL;        // current measured temperature\n    temp_max : REAL;           // alarm trip threshold\n    temp_hyst : REAL;          // hysteresis band (e.g. 2.0)\n    hist_reset : BOOL;         // manual reset of the latched alarm\nEND_VAR\n\nVAR_OUTPUT\n    alarma : BOOL;             // latched high-temperature alarm\n    en_servicio : BOOL;        // TRUE while block is armed\nEND_VAR\n\nVAR\n    alarma_interna : BOOL;     // raw trip flag\nEND_VAR\n\nBEGIN\n    IF temp_actual > temp_max THEN\n        alarma_interna := TRUE;\n    ELSIF temp_actual < (temp_max - temp_hyst) THEN\n        alarma_interna := FALSE;\n    END_IF;\n    IF alarma_interna THEN\n        alarma := TRUE;\n    END_IF;\n    IF hist_reset THEN\n        alarma := FALSE;\n    END_IF;\n    en_servicio := TRUE;\nEND_FUNCTION_BLOCK",
  "advertencias": [
    "Code generated from template 'proteccion_termica': review the logic against the real machine before downloading to the PLC.",
    "The LLM selects the template and parameters; the generator produces exact SCL. The LLM never writes SCL freely (ADR-015)."
  ]
}

$ python3 scripts/cli.py scl validar data/fixtures/scl_good.scl --json
{ "valido": true, "errores": [], "warnings": [...] }

$ python3 scripts/cli.py scl validar data/fixtures/scl_bad.scl --json
{ "valido": false,
  "errores": [
    "unbalanced IF/END_IF (1 vs 0)",
    "missing ';' at end of assignment line: 'alarma := TRUE'",
    "found '=' where ':=' may be required: 'alarma = FALSE'"
  ], "warnings": [...] }

The validator catches exactly the three intentional errors in the bad fixture — deterministic, no LLM involved.

5. TIA Openness bridge (optional, Windows) — honest failure mode

# Bridge URL not configured -> controlled error, never a crash
$ PYTHONPATH=src python3 -c "from plc_ai.openness import OpennessBridgeClient; \
  print(OpennessBridgeClient().health_check())"
{'ok': False, 'error': 'Openness bridge not configured'}

Architecture

                ┌───────────────── Layer 1 · MONITORING ─────────────────┐
                │                                                         │
  S7-1500 ◄─────┤  OPC UA (port 4840)         asyncua simulator ◄────────┤
  (real PLC,    │       │                    (3 machines / 10 tags,       │
  native UA)    │       ▼                      deterministic, seed=42)    │
                │  OpcUaClient.read_tags()  ── value + unit + QUALITY     │
                │       │                                                 │
                │       ▼                                                 │
                │  MCP server (stdio)  ◄── ReAct agent (Ollama LOCAL)     │
                │  7 tools               qwen2.5-coder:7b, urllib-only    │
                └─────────────────────────────────────────────────────────┘
                                             │ selects template (+params)
                                             ▼
                ┌───────────────── Layer 2 · CODE GENERATION ─────────────┐
                │  generate_scl()  →  validate_scl()                      │
                │     (deterministic templates,   (IEC 61131-3 structural │
                │      LLM NEVER writes SCL freely)   validator)          │
                │      │                                                  │
                │      ▼  (optional, Windows only)                        │
                │  Openness bridge (HTTP/JSON) → TIA Portal → PLCSIM      │
                │      │                                                  │
                │      ▼                                                  │
                │  HUMAN REVIEW ──► download to controller (never auto)   │
                └─────────────────────────────────────────────────────────┘

Stack

  • Python 3.11+ (CI runs 3.11 and 3.12)

  • asyncua — OPC UA client/server (the standard Python OPC UA implementation)

  • MCP SDK (mcp>=2.0, MCPServer, stdio transport)

  • Ollama — local LLM (qwen2.5-coder:7b), reachable only inside the Docker network as http://ollama:11434

  • urllib only for the LLM/HTTP clients — no heavyweight LLM SDK

  • Standard library for the CLI, CMMS and history layers

Getting started

python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

pytest tests/ -q                       # 165 tests, no network, no LLM needed

# Diagnostics against the bundled simulator
python3 scripts/cli.py estado
python3 scripts/cli.py tags all
python3 scripts/cli.py alarmas
python3 scripts/cli.py historico motor.temp_C

# Generate history data for historico (example CSV also committed in data/fixtures/)
PYTHONPATH=src python3 -m plc_ai.data_gen --days 7

Full agent run (local LLM via Ollama)

The agent simulates the MCP client: it decides which tools to call, reads the results and answers with grounded diagnostics:

# Inside the Docker network where Ollama lives (see .env.example)
OLLAMA_HOST=ollama:11434 OLLAMA_MODEL=qwen2.5-coder:7b \
    python3 scripts/e2e_agent.py "Mide la temperatura del motor y si es
    alta, genera un bloque SCL de proteccion termica."

No Ollama running? The agent degrades gracefully to a controlled error answer instead of hanging — see docs/LIVE_EVAL.md.

MCP server (for any MCP-capable client)

export PLC_USE_SIMULATOR=true
PYTHONPATH=src python3 -m plc_ai.mcp.server     # stdio

The seven tools:

Tool

Input

Output

leer_tags

tags: [str]

{tags: [{name, value, unit, timestamp, quality}]}

leer_estado

{plc, mode, last_scan_ms, uptime_s}

leer_alarmas

active_only: bool

{alarms: [{id, tag, level, text, since}]}

historial_sensores

tags, start, end, agg

{series: [{tag, points: [{t, v}]}]}

crear_orden_trabajo

maquina, prioridad, descripcion, accion

{id_ot, estado}

generar_scl

objetivo, contexto, plantilla

{scl, advertencias}

validar_scl

scl

{valido, errores, warnings}

Configuration is 100 % env-driven — see .env.example: PLC_ENDPOINT/OPCUA_ENDPOINT, PLC_NAMESPACE/OPCUA_NS, HIST_DATA_DIR, OT_DIR, PLC_USE_SIMULATOR, ALLOW_WRITE_TO_PLC, OLLAMA_HOST, OLLAMA_MODEL, AGENT_MAX_STEPS, OPENNESS_BASE_URL.

Facts / Hypotheses / Estimates

Honest engineering — this is an AI/OT bridge; hardware validation is pending.

Type

Claim

Evidence

FACT

165 tests pass, deterministic

pytest tests/ -q → 165 passed (29 Phase 1 + 24 Phase 2 + 82 Phases 3–4 + 21 CMMS + 9 Openness)

FACT

A real local LLM (Ollama qwen2.5-coder:7b) drove the full loop: read live tags, then generated a valid FB_ProteccionTermica

Live e2e documented in docs/LIVE_EVAL.md (4 real bugs found & fixed)

FACT

The SCL validator detects exactly the 3 intentional errors of scl_bad.scl

scripts/cli.py scl validar output above

FACT

The simulator speaks standard OPC UA on opc.tcp://127.0.0.1:4840; the client reads it with the asyncua library

tests/test_simulator.py + tests/test_client.py (real loopback TCP, in-process)

FACT

TIA Portal Openness requires Windows + a license (V18+); its vendor terms prohibit scripted automation

Reviewed in docs/TIA_OPENNESS_GUIDE.md (official Siemens documentation)

HYPOTHESIS

The same OPC UA client works against a real S7-1500 with the same tag configuration

Not tested — no PLC in the dev environment (tag/namespace mapping documented in docs/REAL_PLC_GUIDE.md)

HYPOTHESIS

The Openness bridge compiles generated SCL in Windows + TIA V20 without changes

Not tested — requires Windows + TIA license

HYPOTHESIS

Simulated tag curves are representative of real machine signals

Validated for signal shapes (sinusoid + drift + injected faults); not against real vibration/temperature spectra

ESTIMATE

OPC UA read latency ~30–100 ms

From asyncua loop timings on a local network (assumption: LAN, low load)

ESTIMATE

One FB compiles in TIA in seconds (order of magnitude)

Not measured — no Windows environment

Limitations

  • No real hardware tested. Everything runs against the deterministic simulator; the real-PLC path is implemented but unverified (see docs/REAL_PLC_GUIDE.md for the exact steps).

  • Compilation stops at syntax on Linux. Layer 2 ends at the SCL validator; a real compile needs Windows + TIA Portal Openness + license.

  • Simulator scope. 3 machines / 10 tags with 9 alarm rules — not every failure mode of a real plant.

  • Local LLMs can hallucinate. Mitigated three ways: the LLM selects one of 8 deterministic templates (it never writes SCL freely), readings are only trusted when quality == 1 and the timestamp is fresh, and the agent is trained (system prompt) to answer with HECHOS/HIPÓTESIS instead of inventing.

  • Single-user, stdio MCP. No HTTP transport, no multi-user session model.

Security

  • No cloud, no API keys. The LLM runs locally (Ollama in the Docker network); nothing leaves the shop floor.

  • Read-only by default. ALLOW_WRITE_TO_PLC=false; there is no write path implemented at all.

  • Never downloads to production autonomously. SCL output requires human review; the optional Openness bridge has no scheduler and no auto-deploy.

  • Only placeholders in .env.example; real .env is gitignored.

Roadmap

  • Validate against a real S7-1500 (tag mapping + namespace; checklist ready in docs/REAL_PLC_GUIDE.md).

  • Close the compile loop on Windows (Openness bridge + PLCSIM).

  • More SCL templates (PID, interlocking), alarm-correlation diagnostics, multi-session MCP (HTTP transport).

plc-ai-agent is part of a public AI-engineering portfolio by eLSeR17. Sibling projects demonstrate complementary skills:

  • alpha-agent — LLM agent with function-calling, guardrails and external evaluations.

  • smart-contract-rag — production RAG over smart-contract audits with golden-dataset evals and a regression guard.

  • evalforge — standalone evaluation framework (dual judge, regression guard) that black-box-evaluates the agent projects.

  • pdm-agent — predictive-maintenance agent: sensor ML pipelines feeding an LLM work-order layer.

Contributing

Issues and PRs are welcome. Keep changes small and testable: every PR must keep pytest green and ruff check clean. Both run in CI.

License

MIT — see LICENSE. The generated SCL is yours; review it before download (that is not a license term, it is the safe thing to do).

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    A
    maintenance
    MCP server that connects AI assistants to Siemens TIA Portal via the Openness API. AI-assisted PLC programming, project management, hardware configuration, cross-reference analysis, and deployment. 19 tools, 230 actions.
    35
    -
  • A
    license
    B
    quality
    A
    maintenance
    Provides AI agents with safe, governed read access to industrial control systems (OPC-UA, Modbus, S7, Mitsubishi, MTConnect, MQTT/Sparkplug) plus cross-protocol diagnostics for troubleshooting data breaks, alarm floods, and unhealthy tags.
    2
    153
    1
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables live tag read/write access to Rockwell Automation Logix5000 controllers over EtherNet/IP without requiring Studio 5000 Logix Designer, including discovery, tag listing, and batch operations.
    MIT