plc-ai-agent
by eLSeR17
README.md
# PLC AI Diagnostic & Code Agent
[](https://github.com/eLSeR17/plc-ai-agent/actions/workflows/ci.yml)
[](https://www.python.org/downloads/)
[](LICENSE)
**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](https://github.com/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.
## 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](#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)
```text
$ 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)
```text
$ 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)
```text
$ 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
```text
$ 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
```text
# 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) │
└─────────────────────────────────────────────────────────┘
```
- Real-PLC integration notes: [`docs/REAL_PLC_GUIDE.md`](docs/REAL_PLC_GUIDE.md)
- TIA Openness integration contract: [`docs/TIA_OPENNESS_GUIDE.md`](docs/TIA_OPENNESS_GUIDE.md)
- Full architecture decision log: [`docs/DECISIONS.md`](docs/DECISIONS.md)
## 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
```bash
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:
```bash
# 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)
```bash
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`](.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).
## Related portfolio projects
`plc-ai-agent` is part of a public AI-engineering portfolio by
[eLSeR17](https://github.com/eLSeR17). Sibling projects demonstrate
complementary skills:
- [`alpha-agent`](https://github.com/eLSeR17/alpha-agent) — LLM agent with
function-calling, guardrails and external evaluations.
- [`smart-contract-rag`](https://github.com/eLSeR17/smart-contract-rag) —
production RAG over smart-contract audits with golden-dataset evals and a
regression guard.
- [`evalforge`](https://github.com/eLSeR17/evalforge) — standalone evaluation
framework (dual judge, regression guard) that black-box-evaluates the agent
projects.
- [`pdm-agent`](https://github.com/eLSeR17/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](LICENSE). The generated SCL is yours; review it before
download (that is not a license term, it is the safe thing to do).This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues