Para ejecutar un servidor OPC UA en un Raspberry PLC, instala Node-RED con los nodos node-red-contrib-iiot-opcua, construye el espacio de direcciones (carpetas y variables) con nodos Server-ASO y arranca un nodo OPCUA-IIoT-Server en un puerto TCP. Después, cualquier cliente OPC UA, como UaExpert o un SCADA, navega y lee las variables en opc.tcp://<IP del PLC>:<puerto>/.
Este ejemplo usa el puerto 55488 (el puerto registrado por defecto para OPC UA es el 4840), escribe cada segundo la entrada analógica I0.10 del PLC en la variable ns=1;s=Variable1 y la vuelve a leer con un cliente de node-red-contrib-opcua. Los tres flujos están más abajo, listos para importar.
¿Qué es OPC UA?
OPC UA (OPC Unified Architecture) es el estándar independiente de plataforma de la OPC Foundation para el intercambio de datos industriales, publicado como IEC 62541. Sustituye a OPC Classic (DA, HDA, A&E), que dependía de COM/DCOM de Windows, y añade un modelo de información, seguridad integrada y transportes que funcionan en cualquier sistema operativo, desde un PLC hasta la nube.
- Independiente de la plataforma: funciona en Linux, Windows y sistemas embebidos, así que un Raspberry PLC puede ser servidor.
- Modelo de información: los datos se organizan como nodos (objetos, variables, métodos) con tipos, unidades y calidad, no como registros planos.
- Seguridad: los certificados de aplicación, la autenticación de usuarios, la firma y el cifrado forman parte del estándar.
- Cliente y servidor, y PubSub: los clientes navegan, leen, escriben y se suscriben a un servidor; PubSub añade publicación y suscripción sobre UDP o MQTT.
Para compararlo con Modbus en un enlace con el SCADA, mira Modbus RTU frente a Modbus TCP frente a OPC UA.
Formas de ejecutar un servidor OPC UA en un Raspberry PLC
| Opción | Lenguaje | Notas |
|---|---|---|
| node-red-contrib-iiot-opcua | Node-RED | Se usa en este ejemplo para el servidor. Versión 4.1.2, probada por sus autores con Node.js 16 y Node-RED 3.0; no se actualiza desde hace unos cuatro años, así que pruébala en tu versión de Node-RED. |
| node-red-contrib-opcua | Node-RED | Se usa en este ejemplo para el cliente. Se mantiene activamente y se basa en node-opcua; también tiene un nodo OpcUa-Server con usuarios, políticas de seguridad y comandos como addFolder y addVariable. |
| asyncua | Python | Librería de código abierto de cliente y servidor; buena opción cuando el resto del programa del PLC está en Python. |
| open62541 | C | Pila OPC UA de código abierto para servidores embebidos y de alto rendimiento. |
| CODESYS | IEC 61131-3 | El runtime de CODESYS incluye un servidor OPC UA; las variables se eligen en la Symbol Configuration. Mira CODESYS en un Raspberry PLC. |
Qué necesitas
- Un Raspberry PLC con su sistema operativo y las librerías de Industrial Shields instaladas. Empieza con los primeros pasos con el Raspberry PLC.
- Node-RED en el PLC, con tres paquetes del gestor de paleta: node-red-contrib-iiot-opcua (servidor), node-red-contrib-opcua (cliente) y los nodos del Raspberry PLC de Industrial Shields,
@industrial-shields/node-red-librpiplc, que aportan los nodos rpiplc-config y rpiplc-analog-read del flujo 2 y necesitan la librería C librpiplc. - node-red-dashboard si quieres ver los indicadores de los flujos 2 y 3.
- Un cliente OPC UA en tu PC para comprobar el servidor, por ejemplo UaExpert de Unified Automation (descarga gratuita con registro).
El espacio de direcciones del ejemplo
El flujo 1 crea una carpeta y dos variables bajo la carpeta estándar Objects (ns=0;i=85). Los flujos 2 y 3 usan el mismo NodeId para escribir y leer.
| NodeId | Nombre de navegación | Tipo | Contenido |
|---|---|---|---|
ns=1;s=TestVariables | Variables | FolderType | Carpeta que organiza las variables |
ns=1;s=TestDateTime | TestDateTime | BaseDataVariableType, DateTime | Fecha y hora inyectadas al arrancar |
ns=1;s=Variable1 | Variable1 | BaseDataVariableType, Int16 | Valor de la entrada analógica I0.10, escrito cada segundo por el flujo 2 |
Paso 1: crea el servidor OPC UA y su espacio de direcciones
Estos nodos crean el servidor, su carpeta y sus variables, y les dan un valor inicial. Después de Deploy, el servidor tarda unos segundos en arrancar: los nodos inject de las variables esperan 15 segundos para que la carpeta exista antes.

- OPCUA-IIoT-Inject envía un mensaje una vez al arrancar, con el tipo inject.

- OPCUA-IIoT-Server-ASO añade un nodo al espacio de direcciones: su NodeId, nombre de navegación, nodo padre y tipo. La carpeta usa FolderType, y las variables usan BaseDataVariableType con un tipo de dato.

- OPCUA-IIoT-Server arranca el servidor en el puerto que indiques, aquí 55488, así que el endpoint es opc.tcp://<IP del PLC>:55488/. También fija límites, descubrimiento y seguridad. El ejemplo permite acceso anónimo sin seguridad, lo que solo vale para un banco de pruebas.

JSON del flujo 1: servidor OPC UA (importar en Node-RED)
[
{
"id": "526638e31bd911fe",
"type": "tab",
"label": "Flow 1",
"disabled": false,
"info": "",
"env": []
},
{
"id": "75df2f23680c144e",
"type": "OPCUA-IIoT-Server",
"z": "526638e31bd911fe",
"port": "55488",
"endpoint": "",
"acceptExternalCommands": true,
"maxAllowedSessionNumber": "",
"maxConnectionsPerEndpoint": "",
"maxAllowedSubscriptionNumber": "",
"alternateHostname": "",
"name": "",
"showStatusActivities": false,
"showErrors": false,
"asoDemo": false,
"allowAnonymous": true,
"individualCerts": false,
"isAuditing": false,
"serverDiscovery": true,
"users": [],
"xmlsets": [],
"publicCertificateFile": "",
"privateCertificateFile": "",
"registerServerMethod": "1",
"discoveryServerEndpointUrl": "",
"capabilitiesForMDNS": "",
"maxNodesPerRead": "",
"maxNodesPerBrowse": "",
"delayToClose": "",
"x": 1034.3000183105469,
"y": 389.9999694824219,
"wires": [
[
"1b7404249f5bac6e"
]
]
},
{
"id": "1b7404249f5bac6e",
"type": "debug",
"z": "526638e31bd911fe",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 1214.3000183105469,
"y": 389.9999694824219,
"wires": []
},
{
"id": "a94182acacad6858",
"type": "OPCUA-IIoT-Server-ASO",
"z": "526638e31bd911fe",
"nodeId": "ns=1;s=TestVariables",
"browsename": "Variables",
"displayname": "Variables",
"objecttype": "FolderType",
"datatype": "",
"value": "",
"referenceNodeId": "ns=0;i=85",
"referencetype": "Organizes",
"name": "Folder TestVariables",
"x": 540,
"y": 340,
"wires": [
[
"2c72a02e95e2e53a"
]
]
},
{
"id": "57a7deb0b12064d7",
"type": "OPCUA-IIoT-Inject",
"z": "526638e31bd911fe",
"injectType": "inject",
"payload": "",
"payloadType": "date",
"topic": "",
"repeat": "",
"crontab": "",
"once": true,
"startDelay": "",
"name": "",
"addressSpaceItems": [],
"x": 250,
"y": 340,
"wires": [
[
"a94182acacad6858"
]
]
},
{
"id": "09ed80c457c3501a",
"type": "OPCUA-IIoT-Server-ASO",
"z": "526638e31bd911fe",
"nodeId": "ns=1;s=TestDateTime",
"browsename": "TestDateTime",
"displayname": "Test DateTime",
"objecttype": "BaseDataVariableType",
"datatype": "DateTime",
"value": "",
"referenceNodeId": "ns=1;s=TestVariables",
"referencetype": "Organizes",
"name": "DateTime",
"x": 580,
"y": 380,
"wires": [
[
"2c72a02e95e2e53a"
]
]
},
{
"id": "f824a67fc665769f",
"type": "OPCUA-IIoT-Inject",
"z": "526638e31bd911fe",
"injectType": "inject",
"payload": "",
"payloadType": "date",
"topic": "",
"repeat": "",
"crontab": "",
"once": true,
"startDelay": "15",
"name": "",
"addressSpaceItems": [],
"x": 250,
"y": 380,
"wires": [
[
"09ed80c457c3501a"
]
]
},
{
"id": "2c72a02e95e2e53a",
"type": "function",
"z": "526638e31bd911fe",
"name": "thru",
"func": "\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 854.3000183105469,
"y": 389.9999694824219,
"wires": [
[
"75df2f23680c144e"
]
]
},
{
"id": "9b7c6cf023d4b643",
"type": "OPCUA-IIoT-Server-ASO",
"z": "526638e31bd911fe",
"nodeId": "ns=1;s=Variable1",
"browsename": "Variable1",
"displayname": "Variable1",
"objecttype": "BaseDataVariableType",
"datatype": "Int16",
"value": "",
"referenceNodeId": "ns=1;s=TestVariables",
"referencetype": "Organizes",
"name": "Variable1Int16",
"x": 560,
"y": 420,
"wires": [
[
"2c72a02e95e2e53a"
]
]
},
{
"id": "9f0e92a8743be1eb",
"type": "OPCUA-IIoT-Inject",
"z": "526638e31bd911fe",
"injectType": "inject",
"payload": "125",
"payloadType": "num",
"topic": "",
"repeat": "",
"crontab": "",
"once": true,
"startDelay": "15",
"name": "",
"addressSpaceItems": [],
"x": 270,
"y": 420,
"wires": [
[
"9b7c6cf023d4b643"
]
]
}
]
Paso 2: escribe una entrada del PLC en el servidor OPC UA
Cada segundo, este flujo lee la entrada analógica I0.10 del Raspberry PLC, la muestra en el dashboard y la escribe en la variable ns=1;s=Variable1. El nodo rpiplc-config selecciona el modelo de PLC, aquí RPIPLC_21. Después de importar el flujo, abre rpiplc-config y selecciona la versión y el modelo de tu PLC.

- OPCUA-IIoT-Node de tipo write fija el NodeId de destino y el tipo de dato, Int16.

- OPCUA-IIoT-Write envía el valor al servidor a través de un conector.

El conector guarda el endpoint. Como el servidor se ejecuta en el mismo PLC, apunta a opc.tcp://localhost:55488/.

El valor es una cuenta bruta del ADC, no voltios. El nodo rpiplc-analog-read devuelve un entero: hasta 12 bits (0 a 4095) en las versiones 3 a 6 del Raspberry PLC. Escálalo antes de publicarlo en unidades de ingeniería. En el flujo de abajo el indicador llega a 4095 y se llama Raw.
JSON del flujo 2: escribir I0.10 en el servidor (importar en Node-RED)
[
{
"id": "63f010ebc6ec597c",
"type": "tab",
"label": "Flow 2",
"disabled": false,
"info": "",
"env": []
},
{
"id": "272bd91f019aaa4a",
"type": "rpiplc-analog-read",
"z": "63f010ebc6ec597c",
"rpiplc": "9440adac8659dc1d",
"pin": "I0.10",
"name": "",
"x": 540,
"y": 340,
"wires": [
[
"72bb01129893f1ae",
"27654d48016fa1bf"
]
]
},
{
"id": "4591787bbefbd0b3",
"type": "OPCUA-IIoT-Write",
"z": "63f010ebc6ec597c",
"connector": "875fa40fca5ccc47",
"name": "",
"justValue": true,
"showStatusActivities": false,
"showErrors": false,
"x": 850,
"y": 340,
"wires": [
[
"e4d285975b908c13"
]
]
},
{
"id": "e4d285975b908c13",
"type": "debug",
"z": "63f010ebc6ec597c",
"name": "debug-set",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 1040,
"y": 340,
"wires": []
},
{
"id": "62aeb4828a504caa",
"type": "inject",
"z": "63f010ebc6ec597c",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "1",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 370,
"y": 340,
"wires": [
[
"272bd91f019aaa4a"
]
]
},
{
"id": "72bb01129893f1ae",
"type": "OPCUA-IIoT-Node",
"z": "63f010ebc6ec597c",
"injectType": "write",
"nodeId": "ns=1;s=Variable1",
"datatype": "Int16",
"value": "",
"name": "",
"topic": "",
"showErrors": false,
"x": 690,
"y": 340,
"wires": [
[
"4591787bbefbd0b3"
]
]
},
{
"id": "27654d48016fa1bf",
"type": "ui_gauge",
"z": "63f010ebc6ec597c",
"name": "",
"group": "0cfb93c85cf539c8",
"order": 0,
"width": 0,
"height": 0,
"gtype": "gage",
"title": "I0.10",
"label": "Raw",
"format": "{{value}}",
"min": 0,
"max": "4095",
"colors": [
"#00b500",
"#e6e600",
"#ca3838"
],
"seg1": "",
"seg2": "",
"diff": false,
"className": "",
"x": 690,
"y": 280,
"wires": []
},
{
"id": "9440adac8659dc1d",
"type": "rpiplc-config",
"model": "RPIPLC_21",
"name": ""
},
{
"id": "875fa40fca5ccc47",
"type": "OPCUA-IIoT-Connector",
"discoveryUrl": "",
"endpoint": "opc.tcp://localhost:55488/",
"endpointMustExist": false,
"keepSessionAlive": true,
"loginEnabled": false,
"name": "LOCAL SERVER",
"showErrors": false,
"securityPolicy": "None",
"securityMode": "None",
"individualCerts": false,
"publicCertificateFile": "",
"privateKeyFile": "",
"defaultSecureTokenLifetime": "",
"autoSelectRightEndpoint": false,
"strategyMaxRetry": "",
"strategyInitialDelay": "",
"strategyMaxDelay": "",
"strategyRandomisationFactor": "",
"requestedSessionTimeout": "",
"connectionStartDelay": "",
"reconnectDelay": "",
"maxBadSessionRequests": "10"
},
{
"id": "0cfb93c85cf539c8",
"type": "ui_group",
"name": "Default",
"tab": "39450c9e599a8dde",
"order": 1,
"disp": true,
"width": "6",
"collapse": false,
"className": ""
},
{
"id": "39450c9e599a8dde",
"type": "ui_tab",
"name": "Home",
"icon": "dashboard",
"disabled": false,
"hidden": false
}
]
Paso 3: lee la variable con un cliente OPC UA
Este flujo usa el otro paquete, node-red-contrib-opcua, como cliente. Cada segundo pide al servidor ns=1;s=Variable1 y muestra el valor en el panel de depuración y en el dashboard. Puede ejecutarse en el mismo PLC o en otro dispositivo de la red.

- OpcUa-Item fija el NodeId que se lee y su tipo de dato.

- OpcUa-Client se conecta al endpoint, aquí opc.tcp://10.10.10.20:55488/ (la dirección eth0 por defecto del Raspberry PLC), con la acción read. El mismo nodo también puede escribir, suscribirse, monitorizar y navegar.

JSON del flujo 3: leer la variable con un cliente (importar en Node-RED)
[
{
"id": "e0635bf35b9bc5c7",
"type": "tab",
"label": "Flow 3",
"disabled": false,
"info": "",
"env": []
},
{
"id": "f7aab83e0a06b0ba",
"type": "OpcUa-Item",
"z": "e0635bf35b9bc5c7",
"item": "ns=1;s=Variable1",
"datatype": "Int16",
"value": "",
"name": "",
"x": 480,
"y": 280,
"wires": [
[
"c9cbb2df0b559691"
]
],
"icon": "font-awesome/fa-tag"
},
{
"id": "c9cbb2df0b559691",
"type": "OpcUa-Client",
"z": "e0635bf35b9bc5c7",
"endpoint": "9911d4815d3e3bdd",
"action": "read",
"deadbandtype": "a",
"deadbandvalue": 1,
"time": 10,
"timeUnit": "s",
"certificate": "n",
"localfile": "",
"localkeyfile": "",
"folderName4PKI": "",
"useTransport": false,
"maxChunkCount": "",
"maxMessageSize": "",
"receiveBufferSize": "",
"sendBufferSize": "",
"name": "",
"x": 680,
"y": 280,
"wires": [
[
"95de452cad37e33f",
"4faa4ea77b213280"
],
[]
],
"icon": "font-awesome/fa-tasks"
},
{
"id": "9e76fd28ede4da39",
"type": "inject",
"z": "e0635bf35b9bc5c7",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "1",
"crontab": "",
"once": true,
"onceDelay": "15",
"topic": "",
"payload": "1",
"payloadType": "num",
"x": 270,
"y": 280,
"wires": [
[
"f7aab83e0a06b0ba"
]
]
},
{
"id": "95de452cad37e33f",
"type": "debug",
"z": "e0635bf35b9bc5c7",
"name": "debug-get",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 900,
"y": 300,
"wires": []
},
{
"id": "4faa4ea77b213280",
"type": "ui_gauge",
"z": "e0635bf35b9bc5c7",
"name": "",
"group": "0cfb93c85cf539c8",
"order": 2,
"width": 0,
"height": 0,
"gtype": "gage",
"title": "Server Read",
"label": "Raw",
"format": "{{value}}",
"min": 0,
"max": "4095",
"colors": [
"#00b500",
"#e6e600",
"#ca3838"
],
"seg1": "",
"seg2": "",
"diff": false,
"className": "",
"x": 910,
"y": 260,
"wires": []
},
{
"id": "9911d4815d3e3bdd",
"type": "OpcUa-Endpoint",
"endpoint": "opc.tcp://10.10.10.20:55488/",
"secpol": "None",
"secmode": "None",
"none": true,
"login": false,
"usercert": false,
"usercertificate": "",
"userprivatekey": ""
},
{
"id": "0cfb93c85cf539c8",
"type": "ui_group",
"name": "Default",
"tab": "39450c9e599a8dde",
"order": 1,
"disp": true,
"width": "6",
"collapse": false,
"className": ""
},
{
"id": "39450c9e599a8dde",
"type": "ui_tab",
"name": "Home",
"icon": "dashboard",
"disabled": false,
"hidden": false
}
]
Comprueba el servidor con UaExpert
- Conecta el PC a la misma red que el PLC y comprueba que responde al ping.
- En UaExpert, añade un servidor con la URL personalizada opc.tcp://<IP del PLC>:55488/ y elige el endpoint con seguridad None, como está configurado en este ejemplo.
- Conecta y abre Objects y después Variables en el panel del espacio de direcciones.
- Arrastra Variable1 a la vista de acceso a datos: el valor cambia cuando cambia la tensión en I0.10.
Antes de producción: protege el servidor
El ejemplo acepta clientes anónimos con política de seguridad None. En la red real de una planta:
- Activa una política de seguridad como Basic256Sha256 con el modo Sign and Encrypt, y confía solo en los certificados de los clientes conocidos.
- Desactiva el acceso anónimo y crea usuarios con contraseña.
- Abre el puerto del servidor solo hacia la red del SCADA o del MES, con el cortafuegos del PLC o de la red.
- Mantén actualizados Node-RED, sus paquetes y el sistema operativo, y protege el editor de Node-RED con usuario y contraseña.
Problemas habituales
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
| El cliente no se conecta | IP o puerto incorrectos, cortafuegos, o el servidor aún está arrancando | Haz ping al PLC, revisa el puerto en el nodo Server y espera unos segundos después del Deploy |
| BadNodeIdUnknown al escribir o leer | La variable aún no existe, o el NodeId está escrito de otra forma | Mantén el retardo de 15 segundos del flujo 1 y copia el NodeId exacto, con el espacio de nombres |
| BadTypeMismatch | El tipo de dato de la escritura no coincide con el de la variable | Usa el mismo tipo (aquí Int16) en Server-ASO, OPCUA-IIoT-Node y OpcUa-Item |
| El valor parece incorrecto | Es la cuenta bruta del ADC | Escálalo a voltios o unidades de ingeniería antes de publicarlo |
Más sobre OPC UA, SCADA y el Raspberry PLC
- Cómo instalar y usar Rapid SCADA en Touchberry Pi
- Guía de Modbus TCP y RTU para PLC Raspberry Pi
- Node-RED: leer entradas analógicas del Raspberry PLC
- Familia Raspberry PLC
- Librerías: node-red-contrib-iiot-opcua, node-red-contrib-opcua, @industrial-shields/node-red-librpiplc. Referencia: OPC UA en Wikipedia.
Llévalo a la práctica
OPC UA es una forma de pasar datos del PLC a un supervisor. Estas páginas muestran cómo un Raspberry PLC comparte sus datos de otras maneras: un gateway para un SCADA, una simulación de planta y un lector Modbus TCP.
Caso de estudio
Cada seguidor informa. El SCADA ve todo el campo.
Un gateway por bloque publica cada fila de seguidores en un mapa de registros Modbus TCP fijo para el SCADA, y por MQTT para el servidor de operaciones.
Lee el caso de estudio
Simulador
Simulador de campo solar: seguidores de un eje, modo difuso, alarmas y limitaciones de red
Una planta de seguidores de un eje de 12,5 MWp monitorizada con controladores de zona Raspberry PLC, un gateway GateBerry y un panel TouchBerry.
Abre el simulador
PLC Solution Library
Leer un inversor Fronius por SunSpec Modbus TCP
Una Raspberry Pi lee la potencia AC de un inversor y el consumo del Smart Meter por Modbus TCP, para calcular el excedente solar en vivo.
Ver el ejemplo
Preguntas frecuentes sobre OPC UA en un Raspberry PLC
¿Una Raspberry Pi puede ser un servidor OPC UA?
Sí. OPC UA es independiente de la plataforma, y una Raspberry Pi o un Raspberry PLC puede ejecutar un servidor con Node-RED (node-red-contrib-iiot-opcua o node-red-contrib-opcua), Python (asyncua), C (open62541) o el servidor OPC UA del runtime de CODESYS.
¿Qué puerto usa OPC UA?
El puerto registrado por defecto para opc.tcp es el 4840, pero un servidor puede escuchar en cualquier puerto TCP libre. Este ejemplo usa el 55488, así que el endpoint es opc.tcp://<IP del PLC>:55488/.
¿Qué diferencia hay entre node-red-contrib-iiot-opcua y node-red-contrib-opcua?
Los dos llevan OPC UA a Node-RED. node-red-contrib-iiot-opcua construye aquí el servidor con nodos Server-ASO, pero no se actualiza desde hace unos cuatro años. node-red-contrib-opcua se mantiene activamente y tiene nodos de cliente y de servidor, así que es la opción más segura para proyectos nuevos.
¿Cómo pruebo un servidor OPC UA?
Conecta un cliente como UaExpert a opc.tcp://<IP del servidor>:<puerto>/, navega por la carpeta Objects y añade las variables a la vista de acceso a datos. Si los valores cambian, el servidor funciona.
¿OPC UA es mejor que Modbus TCP?
Hace más cosas: nodos con tipo y estructura, seguridad integrada y suscripciones. Modbus TCP es más ligero y más fácil de poner en marcha. Muchas plantas usan Modbus a nivel de dispositivo y OPC UA hacia el SCADA, el MES o la nube.
¿Es seguro el acceso anónimo con seguridad None?
Solo en un banco de pruebas aislado. En producción, usa una política de seguridad con Sign and Encrypt, certificados de confianza, cuentas de usuario y un cortafuegos que abra el puerto solo a la red del SCADA.