Ejemplo práctico de comunicación OPC UA con Node.js para integración IT/OT. Lectura de PLCs, suscripción a eventos en tiempo real, y dashboard web con WebSockets para monitoreo de piso de producción.
OPC UA (Unified Architecture) es el estándar de comunicación industrial para conectar sistemas IT (ERP, MES, dashboards) con sistemas OT (PLCs, SCADA, sensores).
graph LR
subgraph "🖥️ MUNDO IT"
DASH["Dashboard<br/>React / Web"]
MES["MES / ERP"]
end
subgraph "🔧 BRIDGE"
NODE["Node.js<br/>Bridge"]
end
subgraph "🏭 MUNDO OT"
OPC["OPC UA Server<br/>(Kepware)"]
PLC1["PLC<br/>Allen-Bradley"]
PLC2["PLC<br/>Siemens"]
SENSORS["Sensores<br/>HMI"]
SCADA["SCADA"]
end
DASH <-->|WebSocket| NODE
MES <-->|REST API| NODE
NODE <-->|OPC UA| OPC
OPC --- PLC1
OPC --- PLC2
OPC --- SENSORS
OPC --- SCADA
style DASH fill:#1e293b,stroke:#38bdf8,color:#f1f5f9
style MES fill:#1e293b,stroke:#38bdf8,color:#f1f5f9
style NODE fill:#334155,stroke:#22c55e,color:#f1f5f9
style OPC fill:#44403c,stroke:#f97316,color:#f1f5f9
style PLC1 fill:#44403c,stroke:#f97316,color:#f1f5f9
style PLC2 fill:#44403c,stroke:#f97316,color:#f1f5f9
style SENSORS fill:#44403c,stroke:#f97316,color:#f1f5f9
style SCADA fill:#44403c,stroke:#f97316,color:#f1f5f9
graph LR
PLC["PLC<br/>Allen-Bradley /<br/>Siemens"] -->|"EtherNet/IP<br/>Profinet"| KEP["Kepware<br/>OPC Server"]
KEP -->|"OPC UA<br/>tcp/49320"| BRIDGE["Node.js<br/>Bridge"]
BRIDGE -->|"WebSocket<br/>REST API"| FRONT["Dashboard<br/>MES / ERP"]
style PLC fill:#44403c,stroke:#f97316,color:#f1f5f9
style KEP fill:#334155,stroke:#eab308,color:#f1f5f9
style BRIDGE fill:#1e3a5f,stroke:#22c55e,color:#f1f5f9
style FRONT fill:#1e293b,stroke:#38bdf8,color:#f1f5f9
📦 opcua-nodejs-industrial
├── 📂 src/
│ ├── 📂 opcua/
│ │ ├── client.ts # Conexión OPC UA
│ │ ├── browser.ts # Explorar nodos del servidor
│ │ ├── reader.ts # Lectura de variables
│ │ ├── subscriber.ts # Suscripción a cambios en tiempo real
│ │ └── writer.ts # Escritura a PLCs (con confirmación)
│ │
│ ├── 📂 bridge/
│ │ ├── opcua-to-ws.ts # Puente OPC UA → WebSocket
│ │ ├── data-logger.ts # Registro en PostgreSQL
│ │ └── alarm-handler.ts # Manejo de alarmas industriales
│ │
│ ├── 📂 api/
│ │ ├── routes.ts # REST API para datos históricos
│ │ └── health.ts # Health check de conexión OPC UA
│ │
│ ├── 📂 dashboard/
│ │ └── index.html # Dashboard de monitoreo (ejemplo)
│ │
│ ├── config.ts # Configuración de nodos y conexiones
│ └── server.ts # Entry point
│
├── 📂 simulation/
│ └── opcua-server.ts # Servidor OPC UA simulado (para desarrollo)
│
├── 📂 docs/
│ ├── kepware-setup.md # Guía de configuración Kepware
│ ├── plc-addressing.md # Tabla de direcciones PLC comunes
│ └── architecture.md
│
├── docker-compose.yml
├── .env.example
└── README.md
# Levantar simulador + PostgreSQL + dashboard
docker compose up -d
# El simulador OPC UA estará en: opc.tcp://localhost:4840
# El dashboard estará en: http://localhost:3000sequenceDiagram
participant PLC as 🏭 PLC
participant OPC as 📡 OPC UA Server
participant NODE as 🔧 Node.js Bridge
participant WS as 🌐 WebSocket
participant DASH as 🖥️ Dashboard
NODE->>OPC: Suscribirse a Temperature, Pressure
loop Cada cambio de valor
PLC->>OPC: Temperatura = 85°C
OPC->>NODE: DataChange notification
NODE->>NODE: ¿Alarma? (> 95°C = crítico)
NODE->>WS: Broadcast a clientes
WS->>DASH: Actualizar gauge
end
Note over PLC,DASH: Latencia total: < 500ms
import { OpcuaClient } from './opcua/client'
const client = new OpcuaClient({
endpoint: 'opc.tcp://localhost:4840',
// Para Kepware en producción:
// endpoint: 'opc.tcp://kepware-server:49320',
security: {
mode: 'SignAndEncrypt',
policy: 'Basic256Sha256',
},
reconnect: {
enabled: true,
interval: 5000,
maxRetries: Infinity, // Producción 24/7: nunca dejar de intentar
}
})
await client.connect()import { OpcuaSubscriber } from './opcua/subscriber'
const subscriber = new OpcuaSubscriber(client, {
publishingInterval: 500, // Updates cada 500ms
})
subscriber.on('ns=2;s=Channel1.Device1.Temperature', (data) => {
console.log(`Temperatura: ${data.value}°C`)
// Enviar a dashboard via WebSocket
wss.broadcast({ type: 'temperature', value: data.value })
// Guardar en histórico
dataLogger.log('temperature', data.value, data.timestamp)
})
await subscriber.start()const values = await reader.readMany([
'ns=2;s=Channel1.Device1.Temperature',
'ns=2;s=Channel1.Device1.Pressure',
'ns=2;s=Channel1.Device1.MotorSpeed',
'ns=2;s=Channel1.Device1.ConveyorStatus',
])const result = await writer.write(
'ns=2;s=Channel1.Device1.MotorSpeed',
750 // RPM
)
// result.status === 'Good' → Escritura exitosagraph TD
VALUE["Valor<br/>recibido"] --> CHECK{"Evaluar<br/>umbrales"}
CHECK -->|"< warnLow o > warnHigh"| WARNING["🟡 Warning"]
CHECK -->|"< alarmLow o > alarmHigh"| CRITICAL["🔴 Critical"]
CHECK -->|"En rango normal"| NORMAL["🟢 Normal"]
WARNING --> LOG["📝 Log"]
CRITICAL --> LOG
CRITICAL --> EMAIL["📧 Email<br/>a supervisor"]
CRITICAL --> SOUND["🔊 Alarma<br/>sonora"]
NORMAL --> DASH["Dashboard"]
style NORMAL fill:#22c55e,stroke:#16a34a,color:#fff
style WARNING fill:#eab308,stroke:#ca8a04,color:#000
style CRITICAL fill:#ef4444,stroke:#dc2626,color:#fff
export const nodes: NodeConfig[] = [
{
nodeId: 'ns=2;s=Channel1.Device1.Temperature',
name: 'Temperatura Horno',
unit: '°C',
warnLow: 60, warnHigh: 85,
alarmLow: 50, alarmHigh: 95,
log: true,
interval: 1000,
},
{
nodeId: 'ns=2;s=Channel1.Device1.Pressure',
name: 'Presión Línea',
unit: 'PSI',
warnHigh: 120,
alarmHigh: 150,
log: true,
interval: 2000,
},
]En manufactura, el sistema debe correr 24/7:
- Detección de desconexión en < 3 segundos
- Reintento cada 5 segundos indefinidamente
- Re-establecimiento automático de todas las suscripciones al reconectar
- Datos perdidos durante desconexión marcados como
quality: 'Uncertain'
Este ejemplo refleja la arquitectura que uso en proyectos de manufactura como Pulso (MES para semiremolques), GymSys (integración biométrica), y mis años como MES/IT-OT Integration Engineer en Capgemini/Essity y LTI, donde conecté PLCs Allen-Bradley y Siemens con sistemas MES y SCADA vía OPC UA y Kepware.
Carlos Clemente Olivares Senior Software Engineer | IT/OT Integration Specialist · 25+ años