Chapter 197: OPC UA Client Implementation
Chapter Objectives
Upon completing this chapter, you will be able to:
- Understand the fundamental concepts of OPC UA (Open Platform Communications Unified Architecture).
- Explain the role and architecture of an OPC UA client.
- Set up an ESP-IDF project with the necessary components for OPC UA communication.
- Implement a basic OPC UA client on an ESP32 to connect to an OPC UA server.
- Read and write data variables from/to an OPC UA server.
- Subscribe to data changes on an OPC UA server.
- Understand common issues and troubleshooting techniques for OPC UA client development on ESP32.
- Recognize differences in implementing OPC UA clients across various ESP32 variants.
Introduction
In the era of Industry 4.0 and the Industrial Internet of Things (IIoT), seamless and secure data exchange between industrial devices, machines, and enterprise systems is paramount. OPC UA (Open Platform Communications Unified Architecture) has emerged as a leading standard for achieving this interoperability. It is a platform-independent, service-oriented architecture that enables secure and reliable data exchange in industrial automation.
While powerful industrial PCs or servers often act as OPC UA clients or servers, the increasing intelligence of edge devices opens up new possibilities. Microcontrollers like the ESP32, with their robust processing capabilities, network connectivity, and cost-effectiveness, can now participate in OPC UA ecosystems, often as clients gathering data from industrial assets or providing data to higher-level systems.
This chapter will guide you through the process of developing an OPC UA client application on an ESP32 microcontroller using the ESP-IDF v5.x framework. We will explore the theoretical underpinnings of OPC UA from a client’s perspective and then dive into practical implementation, enabling your ESP32 to communicate with OPC UA servers in an industrial environment.
Theory
What is OPC UA?
OPC UA is a machine-to-machine communication protocol for industrial automation developed by the OPC Foundation. It is the successor to the original OPC standard (often called OPC Classic), which was based on Microsoft’s COM/DCOM technology and thus limited in terms of platform independence. OPC UA overcomes these limitations by being:
- Platform-Independent: Runs on various operating systems (Windows, Linux, macOS, embedded OS) and hardware.
- Service-Oriented Architecture (SOA): Defines a set of services that clients can invoke on servers.
- Secure: Incorporates robust security mechanisms, including authentication, authorization, encryption, and data integrity checks.
- Extensible Information Model: Allows for the representation of complex data and information structures. Data is organized in an Address Space.
- Multiple Transport Protocols: Can use different transport protocols, most commonly a binary TCP protocol (opc.tcp://) or WebSockets with HTTP/HTTPS.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans'}}}%% graph TD subgraph Industrial Network direction LR S["OPC UA Server <br> <b>(e.g., PLC, Gateway, Industrial PC)</b>"] style S fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E40AF subgraph Server Address Space direction TB O1(Object: Motor) V1(Variable: Speed) V2(Variable: Temperature) M1(Method: StartMotor) O1 -- HasComponent --> V1 O1 -- HasComponent --> V2 O1 -- HasComponent --> M1 end style O1 fill:#FFFFFF,stroke:#4B5563 style V1 fill:#FFFFFF,stroke:#4B5563 style V2 fill:#FFFFFF,stroke:#4B5563 style M1 fill:#FFFFFF,stroke:#4B5563 S --- O1 end subgraph Enterprise/Client Network direction TB C1[<b>SCADA System</b><br>Monitors & Controls] C2[<b>HMI</b><br>Operator Interface] C3[<b>ERP/MES</b><br>Business Logic] C4[<b>ESP32 Client</b><br>Edge Data Collector] end style C1 fill:#EDE9FE,stroke:#5B21B6,stroke-width:1px,color:#5B21B6 style C2 fill:#EDE9FE,stroke:#5B21B6,stroke-width:1px,color:#5B21B6 style C3 fill:#EDE9FE,stroke:#5B21B6,stroke-width:1px,color:#5B21B6 style C4 fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46 C1 -- "Secure Read/Write/Subscribe" --> S C2 -- "Secure Read/Write" --> S C3 -- "Secure Read" --> S C4 -- "Secure Read/Subscribe" --> S classDef default fill:#F9FAFB,stroke:#9CA3AF,stroke-width:1px,color:#374151
OPC UA Core Concepts
Concept | Role in OPC UA | Example |
---|---|---|
Client-Server Model | Defines the fundamental communication architecture. Clients request data, Servers provide it. | An ESP32 (Client) requests the temperature from a PLC (Server). |
Address Space | The structured collection of all data and functions a server exposes to clients. It’s the “map” of the server. | A server’s address space might model an entire factory floor, with objects for each machine. |
Node | The basic building block of the Address Space. Can be a variable, object, method, etc. | A “Temperature” variable with NodeId “ns=2;s=Temp1” is a Node. |
NodeId | A unique identifier for a node within the server’s address space. Essential for accessing specific data. | UA_NODEID_NUMERIC(0, 2258) for Server’s CurrentTime. |
Service | A defined operation that a client can invoke on a server. | Read, Write, Subscribe, Browse, and Call are common services. |
Subscription | An efficient mechanism for a client to receive data updates without constant polling. The server notifies the client of changes. | Subscribing to a pressure variable to get real-time updates when it changes. |
Security Policy | A defined set of cryptographic algorithms and key lengths used to secure communication. | Basic256Sha256 is a common, strong security policy. |
1. Client-Server Architecture
OPC UA operates on a client-server model:
- OPC UA Server: An application that exposes data and functionality. This could be a PLC, a DCS, a sensor, or a gateway device. It manages an Address Space containing data and information.
- OPC UA Client: An application that consumes data and invokes services provided by an OPC UA server. This could be an HMI, a SCADA system, an MES, an ERP system, or, in our case, an ESP32-based application.
2. Address Space and Nodes
The OPC UA server exposes its information through an Address Space. This address space is a collection of Nodes interconnected by References.
- Nodes: Fundamental building blocks representing objects, variables, methods, data types, etc. Each node has attributes, including a unique
NodeId
(which can be a numeric value, a string, a GUID, or an opaque byte string) and aBrowseName
. - Common Node Classes:
Object
: Represents a physical or logical entity (e.g., a motor, a tank).Variable
: Represents data values (e.g., temperature, pressure, setpoint). Variables have aValue
attribute and aDataType
.Method
: Represents callable functions on the server.View
: A subset of the address space, useful for filtering.DataType
: Describes the type of data a variable holds.
- References: Define relationships between nodes, creating a hierarchical or mesh-like structure. For example, an
Object
node might have aHasComponent
reference to severalVariable
nodes.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans'}}}%% graph TD subgraph "OPC UA Server's Address Space" ServerRoot("Objects Folder") style ServerRoot fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF Device1("Object <br> <b>Boiler #1</b> <br> ns=1;s=<i>Boiler1</i>") style Device1 fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E ServerRoot -- "Organizes" --> Device1 subgraph "Boiler #1 Components" TempSensor("Variable <br> <b>Temperature</b> <br> ns=1;s=<i>Boiler1.Temp</i> <br> <i>DataType: Double</i>") PressureSensor("Variable <br> <b>Pressure</b> <br> ns=1;s=<i>Boiler1.Pressure</i> <br> <i>DataType: Double</i>") ValveState("Variable <br> <b>ValveOpen</b> <br> ns=1;s=<i>Boiler1.Valve</i> <br> <i>DataType: Boolean</i>") ResetMethod("Method <br> <b>ResetCounter()</b> <br> ns=1;s=<i>Boiler1.Reset</i>") end style TempSensor fill:#EDE9FE,stroke:#5B21B6,stroke-width:1px,color:#5B21B6 style PressureSensor fill:#EDE9FE,stroke:#5B21B6,stroke-width:1px,color:#5B21B6 style ValveState fill:#EDE9FE,stroke:#5B21B6,stroke-width:1px,color:#5B21B6 style ResetMethod fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B Device1 -- "HasComponent (Reference)" --> TempSensor Device1 -- "HasComponent (Reference)" --> PressureSensor Device1 -- "HasComponent (Reference)" --> ValveState Device1 -- "HasMethod (Reference)" --> ResetMethod end classDef default fill:#F9FAFB,stroke:#9CA3AF,stroke-width:1px,color:#374151
3. Services
Clients interact with servers by invoking Services. OPC UA defines various service sets:
- Discovery Services: Allow clients to find OPC UA servers on the network and their endpoints.
- Secure Channel Services: Establish a secure communication channel between client and server.
- Session Services: Create and manage a session between client and server after a secure channel is established.
- View Services: Allow clients to browse the server’s address space.
- Attribute Services: Read and write attributes of nodes (e.g., the value of a variable).
- Method Services: Call methods on the server.
- Subscription Services: Allow clients to subscribe to data changes or events. The server then sends notifications to the client when subscribed data items change or events occur. This is more efficient than polling.
- Monitored Items: Specific variables or event sources that a client wants to monitor.
- Subscription: A logical grouping of monitored items, with parameters like publishing interval.
4. Information Model
OPC UA allows for rich information modeling. Servers can expose simple data points or complex, structured information representing entire machines or processes. Standard companion specifications (e.g., for PLCs, CNC machines) define common information models for specific device types, promoting interoperability.
5. Security
Security is a cornerstone of OPC UA and is multi-faceted:
- Transport Layer Security:
- UA Secure Conversation (UASC): Uses binary encoding (opc.tcp). Security is established by exchanging X.509 certificates and creating a symmetric key for message signing and encryption.
- HTTPS: For WebSockets transport, leveraging standard web security.
- Security Policies: Define the algorithms used for signing and encryption (e.g.,
None
,Basic256Sha256
). - Message Security Modes:
None
: No security (not recommended for production).Sign
: Messages are signed to ensure integrity.SignAndEncrypt
: Messages are signed and encrypted for integrity and confidentiality.
- User Authentication: Clients must authenticate themselves to the server (e.g., anonymous, username/password, X.509 certificate).
- User Authorization: Servers can restrict access to specific nodes or services based on the authenticated user’s roles and permissions.
The OPC UA Client Role on ESP32
When an ESP32 acts as an OPC UA client, its primary tasks include:
- Network Connection: Establishing a network connection (Wi-Fi or Ethernet) to reach the OPC UA server.
- Discovery (Optional but Recommended): Finding available OPC UA servers and their endpoint descriptions.
- Secure Channel Establishment: Creating a secure communication channel with the chosen server endpoint. This involves certificate exchange if security is enabled.
- Session Creation: Activating a session with the server, including user authentication.
- Browsing (Optional): Navigating the server’s address space to discover available nodes (variables, objects).
- Reading Variables: Requesting the current value of specific variables using their
NodeId
. - Writing Variables: Modifying the value of specific variables on the server (if permitted).
- Subscribing to Data Changes/Events: Creating subscriptions and monitored items to receive real-time updates from the server.
- Method Calls (Less Common for Basic Clients): Invoking methods exposed by the server.
- Closing Session and Secure Channel: Gracefully disconnecting from the server.
open62541: The OPC UA Stack for Embedded Systems
Implementing an OPC UA stack from scratch is a complex undertaking. Fortunately, open-source stacks are available. open62541
is a popular, open-source (MPL v2.0) implementation of OPC UA written in C99, designed to be portable and suitable for embedded systems, including the ESP32. It can be used to build both clients and servers.
The ESP-IDF ecosystem provides open62541
as a manageable component, simplifying its integration into ESP32 projects.
Key features of open62541 relevant to ESP32 clients:
- Supports core OPC UA client services.
- Relatively small footprint (configurable).
- Platform-agnostic, with examples for various systems.
- Actively maintained.
Tip: When working with
open62541
on ESP32, pay close attention to memory usage and task stack sizes, as OPC UA operations, especially with security, can be memory-intensive.
Practical Examples
This section will guide you through creating a basic OPC UA client application on an ESP32. We will use the open62541
library, integrated as an ESP-IDF component.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans'}}}%% flowchart TD A(Start: ESP32 Powers On) --> B{Connect to Wi-Fi}; style A fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6 B --> |Success| C(Initialize <br> OPC UA Client); B --> |Fail| B_Fail(Halt or Retry); style B fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E style B_Fail fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B C --> D{Connect to OPC UA Server <br> opc.tcp://...}; style C fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF style D fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E D --> |Success| E(Connection Established <br> Secure Channel & Session Active); D --> |Fail| D_Fail(Log Error, Cleanup); style E fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46 style D_Fail fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B E --> F{What to do?}; style F fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E F --> |Read Data| G(Read Variable <br> e.g., CurrentTime); F --> |Write Data| H(Write Variable <br> e.g., Setpoint); F --> |Subscribe| I(Create Subscription & <br> Monitored Items); style G fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF style H fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF style I fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF G --> J(Process Value); H --> J; I --> K("Periodically call <br> <b>UA_Client_run_iterate()</b>"); style K fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF K --> L{Data Change <br> Notification?}; style L fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E L -- Yes --> M(Callback function is executed); M --> J; L -- No --> K; J(Process Value / Act) --> N{Continue?}; style J fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46 style N fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E N -- Yes --> F; N -- No --> O(Disconnect & Cleanup); style O fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B O --> P(End); style P fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6
Prerequisites:
- ESP-IDF v5.x toolchain set up with VS Code and the Espressif IDF extension. (Refer to Chapter 2)
- An ESP32 development board.
- A Wi-Fi network that the ESP32 can connect to.
- An OPC UA server for testing. You can use:
- A public test server (e.g.,
opc.tcp://opcuaserver.com:48010
– availability may vary). - A local OPC UA simulation server (e.g., Prosys OPC UA Simulation Server, UaExpert’s built-in server, or even another ESP32 running an OPC UA server example).
- For this example, we’ll assume a server is available at
opc.tcp://<your_server_ip_or_hostname>:<port>
. Replace this with your actual server endpoint.
- A public test server (e.g.,
Step 1: Create a New ESP-IDF Project
- Open VS Code.
- Press
F1
and typeESP-IDF: Show Examples Projects
, press Enter. - Select
Use current ESP-IDF (
your ESP-IDF path)
. - Choose the
hello_world
example from theget-started
section. - Click
Create project using example hello_world
. - Choose a directory to save your new project (e.g.,
esp32_opcua_client
).
Step 2: Add open62541
Component and Configure
open62541
can be added as an ESP-IDF component. The easiest way is often through the component manager if a pre-packaged version is available and suitable. Alternatively, you can add it as a local component. For robust integration, using the component manager is preferred.
- Add open62541 as a dependency:Open a new ESP-IDF terminal in VS Code (Ctrl+Shift+\“ or Terminal > New Terminal, then click the ESP-IDF icon in the terminal list). Navigate to your project directory: cd path/to/your/esp32_opcua_client`Run the following command:
idf.py add-dependency "open62541/open62541^1.3"
This command adds the open62541 component from the ESP-IDF component registry. The version ^1.3 means it will use the latest 1.3.x version. Check the component registry for the latest recommended version if needed.If the above command doesn’t find the component directly (sometimes registry names can vary or specific versions are needed), you might need to search for it on components.espressif.com and use the exact name provided there. An alternative is to manually include it as a submodule in a components directory. - Configure the project for Wi-Fi and OPC UA:Run idf.py menuconfig.
- Wi-Fi Configuration:Navigate to Example Connection Configuration —>.Set your WiFi SSID and WiFi Password.
- open62541 Configuration (if available in menuconfig):Navigate to Component config —> open62541 —>.
- Logging:
UA_LOGLEVEL
can be set toDebug
orInfo
for more verbose output during development. For production,Warning
orError
is better. - Client Settings: Ensure
UA_ENABLE_CLIENT
is enabled. - Memory Allocation: Review memory settings.
UA_ENABLE_CUSTOM_ALLOCATOR
might be useful for fine-tuning, but default settings are often a good start. - Networking: Ensure
UA_ARCHITECTURE_LWIP
is selected if available, or that the networking layer is correctly configured for ESP-IDF. - Disable unused features: To save space, you might disable features like
UA_ENABLE_SERVER
,UA_ENABLE_HISTORIZING
,UA_ENABLE_SUBSCRIPTIONS_EVENTS
if not used. For this example, we will need subscriptions.
- Logging:
- Compiler Options:Navigate to Compiler options —> Optimization Level —>.Consider Optimize for size (-Os) if flash space becomes an issue, but Optimize for performance (-O2) is generally good.
- Increase Main Task Stack Size: OPC UA operations can be stack-intensive.Navigate to Component config —> ESP System Settings —> Main task stack size.Increase it to at least 8192 or 16384 bytes. Start with 8192 and increase if you encounter stack overflows.
menuconfig
.
Step 3: Write the OPC UA Client Code
Replace the content of main/hello_world_main.c
(or your main C file) with the following code.
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "lwip/err.h"
#include "lwip/sys.h"
// open62541
#include "open62541/client.h"
#include "open62541/client_config_default.h"
#include "open62541/client_highlevel.h"
#include "open62541/plugin/log_stdout.h" // For logging
// Wi-Fi Configuration - Replace with your details if not using menuconfig's example connection
#define EXAMPLE_ESP_WIFI_SSID CONFIG_EXAMPLE_WIFI_SSID
#define EXAMPLE_ESP_WIFI_PASS CONFIG_EXAMPLE_WIFI_PASSWORD
#define EXAMPLE_ESP_MAXIMUM_RETRY CONFIG_EXAMPLE_MAXIMUM_RETRY
// OPC UA Server Endpoint URL - REPLACE WITH YOUR SERVER'S ENDPOINT
#define OPCUA_SERVER_ENDPOINT_URL "opc.tcp://opcuaserver.com:48010"
// Example: "opc.tcp://192.168.1.100:4840" or "opc.tcp://milo.digitalpetri.com:62541/milo"
static const char *TAG = "OPCUA_CLIENT";
/* FreeRTOS event group to signal when we are connected*/
static EventGroupHandle_t s_wifi_event_group;
/* The event group allows multiple bits for each event, but we only care about two events:
* - we are connected to the AP with an IP
* - we failed to connect after the maximum amount of retries */
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1
static int s_retry_num = 0;
static void event_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data) {
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
if (s_retry_num < EXAMPLE_ESP_MAXIMUM_RETRY) {
esp_wifi_connect();
s_retry_num++;
ESP_LOGI(TAG, "retry to connect to the AP");
} else {
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
}
ESP_LOGI(TAG,"connect to the AP fail");
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
s_retry_num = 0;
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
}
void wifi_init_sta(void) {
s_wifi_event_group = xEventGroupCreate();
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
esp_event_handler_instance_t instance_any_id;
esp_event_handler_instance_t instance_got_ip;
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
ESP_EVENT_ANY_ID,
&event_handler,
NULL,
&instance_any_id));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
IP_EVENT_STA_GOT_IP,
&event_handler,
NULL,
&instance_got_ip));
wifi_config_t wifi_config = {
.sta = {
.ssid = EXAMPLE_ESP_WIFI_SSID,
.password = EXAMPLE_ESP_WIFI_PASS,
.threshold.authmode = WIFI_AUTH_WPA2_PSK, // Adjust if needed
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
ESP_ERROR_CHECK(esp_wifi_start() );
ESP_LOGI(TAG, "wifi_init_sta finished.");
/* Waiting until either the connection is established (WIFI_CONNECTED_BIT) or connection failed for the maximum
* number of re-tries (WIFI_FAIL_BIT). The bits are set by event_handler() (see above) */
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
pdFALSE,
pdFALSE,
portMAX_DELAY);
if (bits & WIFI_CONNECTED_BIT) {
ESP_LOGI(TAG, "connected to ap SSID:%s", EXAMPLE_ESP_WIFI_SSID);
} else if (bits & WIFI_FAIL_BIT) {
ESP_LOGI(TAG, "Failed to connect to SSID:%s", EXAMPLE_ESP_WIFI_SSID);
} else {
ESP_LOGE(TAG, "UNEXPECTED EVENT");
}
}
static void opcua_client_task(void *pvParameters) {
UA_Client *client = UA_Client_new();
UA_ClientConfig *cc = UA_Client_getConfig(client);
UA_ClientConfig_setDefault(cc);
// For simplicity, we use no security.
// IMPORTANT: For production, configure appropriate security policies and certificates!
// cc->securityMode = UA_MESSAGESECURITYMODE_SIGNANDENCRYPT; // Example for secure mode
// cc->securityPolicyUri = UA_STRING_NULL; // Let server choose or set specific
// cc->clientCertificate = ...
// cc->clientPrivateKey = ...
// cc->serverCertificate = ...
ESP_LOGI(TAG, "Attempting to connect to OPC UA server: %s", OPCUA_SERVER_ENDPOINT_URL);
UA_StatusCode retval = UA_Client_connect(client, OPCUA_SERVER_ENDPOINT_URL);
if (retval != UA_STATUSCODE_GOOD) {
ESP_LOGE(TAG, "Failed to connect to OPC UA server, error: %s", UA_StatusCode_name(retval));
UA_Client_delete(client);
vTaskDelete(NULL);
return;
}
ESP_LOGI(TAG, "Successfully connected to OPC UA server!");
/* Read a value */
UA_Variant value;
UA_Variant_init(&value);
// Example: Read the current server time (a standard OPC UA node)
// NodeId for Server->ServerStatus->CurrentTime: Numeric, Namespace 0, Identifier 2258
const UA_NodeId currentTimeNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME);
ESP_LOGI(TAG, "Reading CurrentTime from server...");
retval = UA_Client_readValueAttribute(client, currentTimeNodeId, &value);
if (retval == UA_STATUSCODE_GOOD && UA_Variant_hasScalarType(&value, &UA_TYPES[UA_TYPES_DATETIME])) {
UA_DateTime raw_date = *(UA_DateTime *)value.data;
UA_DateTimeStruct dts = UA_DateTime_toStruct(raw_date);
ESP_LOGI(TAG, "Current server time: %02u-%02u-%04u %02u:%02u:%02u.%03u",
dts.day, dts.month, dts.year, dts.hour, dts.min, dts.sec, dts.milliSec);
} else {
ESP_LOGE(TAG, "Failed to read CurrentTime or unexpected type, error: %s", UA_StatusCode_name(retval));
}
UA_Variant_clear(&value);
/* Write a value (Example - ensure the node is writable and of correct type on your server) */
// This is a placeholder NodeId. You need to replace it with a writable node on your server.
// For example, if your server has a writable boolean variable at Namespace 1, Identifier "MySwitch"
// const UA_NodeId writableNodeId = UA_NODEID_STRING(1, "MySwitch");
// Or if it's numeric: const UA_NodeId writableNodeId = UA_NODEID_NUMERIC(1, 12345);
// Let's try to write to a hypothetical writable integer node.
// IMPORTANT: Create such a node on your test server or find one.
// For example, NodeId: ns=2;s="MyWritableInteger"
// const UA_NodeId writableIntNodeId = UA_NODEID_STRING(2, "MyWritableInteger"); // Adjust ns and identifier
// UA_Variant myVariant;
// UA_Int32 myIntValue = 123;
// UA_Variant_setScalar(&myVariant, &myIntValue, &UA_TYPES[UA_TYPES_INT32]);
// ESP_LOGI(TAG, "Writing %d to node...", myIntValue);
// retval = UA_Client_writeValueAttribute(client, writableIntNodeId, &myVariant);
// if(retval == UA_STATUSCODE_GOOD) {
// ESP_LOGI(TAG, "Successfully wrote value!");
// } else {
// ESP_LOGE(TAG, "Failed to write value, error: %s", UA_StatusCode_name(retval));
// }
// UA_Variant_clear(&myVariant);
/* Simple Subscription Example */
// We will try to subscribe to the ServerStatus->State node
// NodeId for Server->ServerStatus->State: Numeric, Namespace 0, Identifier UA_NS0ID_SERVER_SERVERSTATUS_STATE (2259)
const UA_NodeId stateNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_STATE);
UA_CreateSubscriptionRequest subRequest = UA_CreateSubscriptionRequest_default();
UA_CreateSubscriptionResponse subResponse = UA_Client_Subscriptions_create(client, subRequest,
NULL, NULL, NULL);
if(subResponse.responseHeader.serviceResult == UA_STATUSCODE_GOOD) {
ESP_LOGI(TAG, "Subscription created with ID: %u", subResponse.subscriptionId);
} else {
ESP_LOGE(TAG, "Failed to create subscription, error: %s", UA_StatusCode_name(subResponse.responseHeader.serviceResult));
UA_Client_disconnect(client);
UA_Client_delete(client);
vTaskDelete(NULL);
return;
}
UA_UInt32 subscriptionId = subResponse.subscriptionId;
UA_MonitoredItemCreateRequest monRequest = UA_MonitoredItemCreateRequest_default(stateNodeId);
// Customize monitoring parameters if needed, e.g., sampling interval
// monRequest.requestedParameters.samplingInterval = 1000.0; // ms
// DataChangeNotification callback
// This is a simplified handler. In a real app, you'd pass context and handle data properly.
static void dataChangeNotificationCallback(UA_Client *client, UA_UInt32 subId, void *subContext,
UA_UInt32 monId, void *monContext, UA_DataValue *value) {
if(UA_Variant_hasScalarType(&value->value, &UA_TYPES[UA_TYPES_INT32])) { // ServerState is an Int32 (enum)
UA_Int32 serverState = *(UA_Int32*)value->value.data;
ESP_LOGI(TAG, "Subscription: Server State changed to: %d", serverState);
// You can look up UA_ServerState enum values in open62541 documentation or opc ua spec
// e.g., 0 = Running, 1 = Failed, 2 = NoConfig, etc.
} else {
ESP_LOGW(TAG, "Subscription: Received data change for ServerState, but type is not Int32 as expected.");
}
}
UA_MonitoredItemCreateResult monResponse = UA_Client_MonitoredItems_createDataChange(
client, subscriptionId, UA_TIMESTAMPSTORETURN_BOTH,
monRequest, NULL, dataChangeNotificationCallback, NULL);
if(monResponse.statusCode == UA_STATUSCODE_GOOD) {
ESP_LOGI(TAG, "Monitoring item created for ServerState with MonitoredItemID: %u", monResponse.monitoredItemId);
} else {
ESP_LOGE(TAG, "Failed to create monitored item for ServerState, error: %s", UA_StatusCode_name(monResponse.statusCode));
// Clean up subscription if item creation fails
UA_Client_Subscriptions_delete(client, subscriptionId);
UA_Client_disconnect(client);
UA_Client_delete(client);
vTaskDelete(NULL);
return;
}
// Keep the client running to receive subscription updates
// In a real application, UA_Client_run_iterate would be called periodically in a loop
// or a dedicated task would handle it.
// For this example, we'll just run it for a while.
ESP_LOGI(TAG, "Client running. Waiting for subscription updates for 60 seconds...");
for(int i=0; i<60; ++i) {
retval = UA_Client_run_iterate(client, 1000); // Timeout in ms
if(retval != UA_STATUSCODE_GOOD && retval != UA_STATUSCODE_BADTIMEOUT) {
ESP_LOGE(TAG, "UA_Client_run_iterate failed: %s", UA_StatusCode_name(retval));
break;
}
// You can add logic here to check for external stop signals
vTaskDelay(pdMS_TO_TICKS(1000)); // Delay 1 second
}
ESP_LOGI(TAG, "Client task finishing. Cleaning up.");
/* Clean up */
// Delete monitored item (optional, as deleting subscription typically handles this)
// UA_Client_MonitoredItems_delete(client, subscriptionId, 1, &monResponse.monitoredItemId);
// Delete subscription
UA_Client_Subscriptions_delete(client, subscriptionId);
UA_Client_disconnect(client);
UA_Client_delete(client);
ESP_LOGI(TAG, "Disconnected and client deleted.");
vTaskDelete(NULL);
}
void app_main(void) {
//Initialize NVS
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_LOGI(TAG, "ESP_WIFI_MODE_STA");
wifi_init_sta(); // Connect to Wi-Fi
// Wait for Wi-Fi connection before starting OPC UA client
EventBits_t bits = xEventGroupGetBits(s_wifi_event_group);
if (bits & WIFI_CONNECTED_BIT) {
ESP_LOGI(TAG, "Wi-Fi Connected. Starting OPC UA client task.");
// Ensure sufficient stack size for the OPC UA task
xTaskCreate(&opcua_client_task, "opcua_client_task", 16384, NULL, 5, NULL);
} else {
ESP_LOGE(TAG, "Wi-Fi not connected. OPC UA client task will not start.");
}
}
Explanation of the Code:
- Includes: Necessary headers for FreeRTOS, Wi-Fi, ESP-IDF logging, NVS, and
open62541
. - Wi-Fi Configuration: Standard ESP-IDF Wi-Fi station mode initialization (
wifi_init_sta
). It waits until a connection is established or fails. OPCUA_SERVER_ENDPOINT_URL
: Crucial: You must replace the placeholder URL with the actual endpoint URL of your OPC UA server.opcua_client_task
:- Client Creation:
UA_Client_new()
creates a new client instance.UA_Client_getConfig()
andUA_ClientConfig_setDefault()
initialize its configuration. - Security (Simplified): The example explicitly uses no security (
UA_MESSAGESECURITYMODE_NONE
). Comments indicate where to configure security policies and certificates for production.Warning: UsingUA_MESSAGESECURITYMODE_NONE
is insecure and should only be used for initial testing in a controlled environment. Always implement proper security for production systems. - Connect:
UA_Client_connect()
attempts to connect to the server. - Read Value:
UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME)
defines theNodeId
for the server’s current time (a standard node). Namespace 0 (UA_NS0ID_...
constants are for this) is the OPC UA base namespace.UA_Client_readValueAttribute()
reads the value.- The result is checked, and if it’s a
DateTime
, it’s printed.
- Write Value (Commented Out):
- The code includes a commented-out section for writing a value.
- You need to identify a writable node on your server (e.g.,
ns=2;s="MyWritableInteger"
if it’s a string identifier in namespace 2, orns=1;i=12345
if it’s a numeric identifier in namespace 1). UA_Variant_setScalar()
prepares the value to be written.UA_Client_writeValueAttribute()
performs the write operation.
- Subscription:
UA_Client_Subscriptions_create()
creates a subscription on the server.UA_MonitoredItemCreateRequest_default()
prepares a request to monitor a specific node (here,ServerStatus->State
,UA_NS0ID_SERVER_SERVERSTATUS_STATE
).dataChangeNotificationCallback
is a function that will be called by the stack when the server sends a notification for the monitored item.UA_Client_MonitoredItems_createDataChange()
registers the monitored item with the subscription.UA_Client_run_iterate()
: This function must be called periodically by the client to process network messages, handle timeouts, and trigger callbacks for subscriptions. The loop runs for 60 seconds, callingrun_iterate
every second. In a robust application, this would be handled more elegantly, perhaps in its own loop within the task or driven by a timer.
- Cleanup: The client disconnects, and resources are freed using
UA_Client_disconnect()
andUA_Client_delete()
. Subscriptions and monitored items should also be explicitly deleted if not handled by client deletion.
- Client Creation:
app_main
:- Initializes NVS (Non-Volatile Storage), which is used by the Wi-Fi driver.
- Calls
wifi_init_sta()
to connect to Wi-Fi. - Once Wi-Fi is connected, it creates the
opcua_client_task
with a stack size of 16384 bytes. This is important as OPC UA operations, especially withopen62541
, can require a significant amount of stack.
Step 4: Modify CMakeLists.txt
Ensure your main CMakeLists.txt
file correctly links the open62541
component if it wasn’t automatically handled by add-dependency
. Typically, add-dependency
updates the build system. If you added open62541
manually to the components
folder, you might need to ensure it’s properly discovered.
The idf_component_register
line in main/CMakeLists.txt
should look something like this:
# main/CMakeLists.txt
idf_component_register(SRCS "hello_world_main.c" # or your main C file name
INCLUDE_DIRS "."
REQUIRES lwip esp_wifi nvs_flash open62541) # Added open62541
The add-dependency
command should handle this by creating a dependencies.cmake
file or similar that links open62541
. If you encounter linking errors for open62541
functions, double-check your project’s CMakeLists.txt
and the CMakeLists.txt
within the open62541
component itself.
Step 5: Build, Flash, and Observe
- Connect your ESP32 board to your computer.
- Select the correct serial port in VS Code (usually auto-detected, or use
ESP-IDF: Select Port to Use (COM, ttyS)
) - Select the target device (e.g., ESP32, ESP32-S3) using
ESP-IDF: Set Espressif Device Target
. - Build the project:Click the “Build” button (cylinder icon) in the VS Code status bar, or run idf.py build in the ESP-IDF terminal.
- Flash the project:Click the “Flash” button (lightning bolt icon), or run idf.py -p (YourPort) flash (e.g., idf.py -p COM3 flash).
- Monitor the output:Click the “Monitor” button (plug icon), or run idf.py -p (YourPort) monitor.
You should see Wi-Fi connection logs, followed by OPC UA client logs:
I (OPCUA_CLIENT): Attempting to connect to OPC UA server: opc.tcp://your_server_ip:port
I (OPCUA_CLIENT): Successfully connected to OPC UA server!
I (OPCUA_CLIENT): Reading CurrentTime from server...
I (OPCUA_CLIENT): Current server time: DD-MM-YYYY HH:MM:SS.mmm
I (OPCUA_CLIENT): Subscription created with ID: <sub_id>
I (OPCUA_CLIENT): Monitoring item created for ServerState with MonitoredItemID: <mon_item_id>
I (OPCUA_CLIENT): Client running. Waiting for subscription updates for 60 seconds...
I (OPCUA_CLIENT): Subscription: Server State changed to: 0 (Example: Running)
... (more subscription updates if the server state changes) ...
I (OPCUA_CLIENT): Client task finishing. Cleaning up.
I (OPCUA_CLIENT): Disconnected and client deleted.
If you uncommented and correctly configured the “Write Value” section, you should also see logs for that operation.
Tip: Use an OPC UA client tool like UaExpert to connect to your test server simultaneously. This allows you to verify the server’s status, browse its address space to find
NodeId
s, and observe value changes caused by your ESP32 client.
Variant Notes
OPC UA client implementation can be demanding on microcontroller resources. Here’s how different ESP32 variants might fare:
ESP32 Variant | Core(s) | RAM/PSRAM | OPC UA Client Suitability | Key Consideration |
---|---|---|---|---|
ESP32 (Original) | Dual-Core Xtensa LX6 | ~520KB SRAM | Good: Capable for most client tasks. RAM is the main constraint for secure connections. | Balance features in open62541 to manage RAM. Needs careful stack size management. |
ESP32-S3 | Dual-Core Xtensa LX7 | ~512KB SRAM + PSRAM | Excellent: More processing power and available PSRAM make it ideal for complex/secure clients. | PSRAM is a major advantage for handling large certificates and data buffers. |
ESP32-C3 | Single-Core RISC-V | ~400KB SRAM | Fair: Suitable for simple clients with minimal security (e.g., read-only, no encryption). | Resource constraints are tight. Aggressively disable unused open62541 features. |
ESP32-C6 | Single-Core RISC-V | ~512KB SRAM | Good: Wi-Fi 6 support is a plus. Good for moderately complex clients. | Similar to original ESP32 in capability but with a more modern radio. |
ESP32-H2 | Single-Core RISC-V | ~320KB SRAM | Limited: Technically possible but not recommended. Very tight on all resources. | Better suited for non-TCP/IP protocols like Thread or Zigbee. |
General Considerations for All Variants:
- Memory (RAM and Flash):
open62541
can be configured at compile time to include/exclude features, impacting its footprint. Secure connections (TLS) add significant overhead due to certificate handling and cryptographic operations. - Processing Power: Secure communications and complex data handling require more CPU cycles.
- Network Stack Performance: The underlying TCP/IP stack (LWIP on ESP-IDF) performance also plays a role.
- Task Stack Size: As demonstrated, OPC UA client tasks often require larger stack sizes (e.g., 8KB-16KB or more) to avoid overflows, especially during connection establishment or when handling complex data structures.
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
Stack Overflow | ESP32 continuously reboots with Guru Meditation Error: Core 0 panic'd (Interrupt wdt timeout on CPU0) or Stack canary watchpoint triggered . |
The OPC UA task needs more stack. Solution: Increase stack size in xTaskCreate from 8192 to 16384 or even 32768 . |
Connection Failed | Log shows Failed to connect to OPC UA server with error BadTimeout or BadCommunicationError . |
|
Subscription Not Updating | Client connects, creates subscription, but the data change callback never fires. No errors shown. | The UA_Client_run_iterate(client, timeout) function is not being called. Solution: Ensure this function is called periodically in a loop within your client task. It’s required to process network events. |
Read/Write Fails | Connection is good, but UA_Client_readValueAttribute or ...writeValueAttribute returns BadNodeIdUnknown . |
The NodeId is incorrect. Solution: Use a client like UaExpert to browse the server’s Address Space. Verify the Namespace Index (ns) and the Identifier (numeric or string) are an exact match. |
Security/Certificate Error | Connection fails with errors like BadSecurityChecksFailed , BadCertificateInvalid , or BadSecurityPolicyRejected . |
|
Exercises
- Read Multiple Variables:Modify the opcua_client_task to read values from three different variables on your OPC UA server. These could be standard server status variables or custom variables you create on your test server. Print their NodeIds and values.
- Write to Different Data Types:If your test server allows, create writable variables of different data types (e.g., Boolean, Float, String). Modify the client to write appropriate values to these nodes and verify the changes using UaExpert or by reading them back.
- Enhanced Subscription Handling:Expand the subscription example. Subscribe to multiple variables. In the dataChangeNotificationCallback, use the monContext or monId parameter to identify which variable changed and print its new value along with a descriptive name.
- Connect to a Secure Server (Advanced):Set up your test OPC UA server with a basic security policy (e.g., Basic256Sha256 with SignAndEncrypt). Generate or obtain the necessary client and server certificates. Modify the ESP32 OPC UA client code to:
- Load the client certificate and private key.
- Trust the server’s certificate.
- Configure the
UA_ClientConfig
with the appropriate security policy and mode. - Successfully connect to the secure server.(This is a challenging exercise and requires a good understanding of X.509 certificates and OPC UA security concepts. Refer to open62541 documentation on security.)
Summary
- OPC UA is a crucial standard for secure, platform-independent industrial communication, based on a client-server architecture.
- Clients interact with servers by invoking services to access an Address Space composed of Nodes (representing variables, objects, etc.).
- The ESP32 can act as an OPC UA client, enabling it to integrate with industrial systems.
- The
open62541
library provides an open-source OPC UA stack suitable for ESP32, manageable as an ESP-IDF component. - Key client operations include connecting, reading/writing variables, and subscribing to data changes.
- Security (authentication, encryption) is a fundamental aspect of OPC UA and must be carefully configured for production.
- Resource constraints (RAM, Flash, CPU) on ESP32 variants, especially for secure connections, must be considered. Adequate task stack size is critical.
- Proper error handling and understanding of NodeIDs are essential for robust client development.
- The
UA_Client_run_iterate()
function is vital for processing asynchronous events like subscription notifications.
Further Reading
- open62541 Documentation:https://open62541.org/doc/current/
- Client Tutorial: https://open62541.org/doc/current/tutorial_client.html
- Security Configuration: Consult sections on certificates and security policies.
- OPC Foundation Website:https://opcfoundation.org/
- OPC UA Specifications: For deep dives into the protocol.
- ESP-IDF Programming Guide:
- ESP-IDF Component Registry: https://components.espressif.com/ (Search for
open62541
or other relevant components).