OPC UA in MFC Applications: Integrating open62541 for Industrial SCADA on Windows
Industrial automation systems are increasingly moving away from monolithic, proprietary SCADA solutions in favor of flexible, secure, and cross-platform OPC UA-based platforms. In this context, developing a lightweight SCADA system based on Microsoft Foundation Classes (MFC) with open62541 integration becomes a technically sound choice for medium-sized enterprises—especially when compatibility with legacy Windows infrastructures and strict latency requirements for communication with Siemens S7-1200/S7-1500 controllers are needed.
Architectural Shift: From Proprietary SCADA to OPC UA–Driven Integration
Traditional SCADA platforms (WinCC, InTouch, EcoStruxure) are built around closed protocols and vendor lock-in. Their deployment requires licensing, extensive configuration, and often results in bloated software installations—setup packages can reach 20 GB, even though only 15–20% of the functionality is actually used. For small and medium-sized businesses, this is economically unjustified.
OPC UA addresses three systemic challenges:
- Interoperability: A unified address space (NodeId), typed variables, and a single subscription mechanism allow data aggregation from equipment by different manufacturers without intermediate gateways.
- Out-of-the-Box Security: Support for X.509 certificates, security policies (None, Basic128Rsa15, Basic256Sha256), and protocol-level encryption and authentication.
- Deployment Flexibility: Clients can be native Windows applications (MFC/Win32), web services, or microservices running on Linux—without modifying the server-side logic.
In industrial practice, this means replacing expensive SCADA systems with custom solutions that connect directly to controllers via OPC UA, integrate with SQL Server for storing recipes and historical data, and enable remote monitoring through charts and equipment status displays.
Integrating open62541 into MFC: Technical Features and Limitations
open62541 is a C99-compliant, zero-dependency open-source library designed for embedded and desktop systems. Using it in an MFC project requires meeting several conditions:
- The project must compile in
/MDmode (dynamic linking to CRT), since open62541 relies on the standard C library. - Explicit initialization of the TLS/SSL stack is required when using secure policies—in Windows, this involves
Wincrypt.handCryptInitialize(). - All objects such as
UA_Client,UA_CreateSubscriptionRequest, andUA_MonitoredItemCreateRequestmust be managed manually: memory allocation, callingUA_Client_disconnect(), and freeing withUA_Client_delete().
A critical limitation is the lack of a built-in thread pool. In an MFC application with a GUI thread, all calls to UA_Client_* must be executed in a separate worker thread (AfxBeginThread) or via PostMessage/WM_USER to avoid blocking the interface. Directly calling UA_Client_connect() in CMainFrame::OnInitialUpdate() will cause the window to freeze.
Implementing Connection and Subscription in C++/MFC
The function CMainFrame::ConnectCPUOnline() demonstrates a minimally viable scenario for connecting to an OPC UA server. It initializes the client, configures settings, and establishes the connection. Key points include:
- Using
UA_String_fromChars()to convertCT2Astrings intoUA_String—a mandatory requirement of the open62541 API. - Setting
cc->securityMode = UA_MESSAGESECURITYMODE_NONEis only acceptable in isolated networks; in real-world projects, you need to chooseUA_MESSAGESECURITYMODE_SIGNANDENCRYPTand load certificates viaUA_ClientConfig_setDefaultEncryption(). - Logging at the
UA_LOGLEVEL_ERRORlevel strikes the optimal balance between diagnostics and performance in production.
After connecting, a subscription is created and monitoring items are configured. Below is a snippet of how to create a subscription and add the first tag:
// Creating a subscription
UA_CreateSubscriptionRequest request = UA_CreateSubscriptionRequest_default();
request.requestedPublishingInterval = 500.0; // ms
request.requestedMaxKeepAliveCount = 10;
request.requestedLifetimeCount = 30;
UA_CreateSubscriptionResponse response = UA_Client_Subscriptions_create(
StrCPUADR[iAdr].g_clientOnline, request, NULL, NULL, NULL);
if (response.responseHeader.serviceResult != UA_STATUSCODE_GOOD) {
return false;
}
StrCPUADR[iAdr].g_subscriptionId = response.subscriptionId;
// Configuring monitoring for the tag
UA_MonitoredItemCreateRequest monRequest = UA_MonitoredItemCreateRequest_default();
monRequest.itemToMonitor.nodeId = UA_NODEID_NUMERIC(0, StrDBINI[i].iIDUA);
monRequest.itemToMonitor.attributeId = UA_ATTRIBUTEID_VALUE;
monRequest.monitoringMode = UA_MONITORINGMODE_REPORTING;
UA_MonitoredItemCreateResult monResponse = UA_Client_MonitoredItems_createSingle(
StrCPUADR[iAdr].g_clientOnline,
StrCPUADR[iAdr].g_subscriptionId,
&monRequest);
Note that iIDUA is the controller’s NodeId obtained from the database (e.g., ns=2;s=Channel1.Device1.Tag1). It must exactly match the server’s address space. Errors in the NodeId result in UA_STATUSCODE_BADNODEIDUNKNOWN, but do not cause a crash—checking monResponse.statusCode is essential.
Working with Data: From Reading to Logging in SQL Server
The system uses three key structures to manage data:
StrDBCP—configuration for connections to controllers: IP address, online/offline status, pointer toUA_Client*, subscription ID, and anarrayValuebuffer for receiving values.StrDBINI—tag metadata:iIDUA(NodeId),iTYPEID(data type:UA_TYPES_INT32,UA_TYPES_DOUBLE,UA_TYPES_BOOLEAN), and abSaveDbflag for logging.StrDBVAL—historical value table with timestamps,CTime,iNOMEQ,iItem, andiVal.
The logic for updating values is implemented in the callback function UA_Client_DataChangeNotification. When a new value is received:
- Check the
iTYPEIDand safely cast the type usingUA_Variant_getScalarCopy(). - Write to
arrayValue[i]taking into accountsizearray. - If
bSaveDb == true, form a parameterized INSERT statement in SQL Server viaCDatabase::ExecuteSQL().
Important: All operations on arrayValue must be protected by a critical section (CCriticalSection), since data comes from a background thread while the GUI thread may simultaneously request it for chart rendering.
What Matters
- OPC UA in an MFC application requires manual management of the
UA_Clientlifecycle: initialization → connection → subscription → reading → unsubscription → disconnection → deletion. - Security is not optional:
SECURITYPOLICY_NONEis only acceptable in test environments; in industrial networks, X.509 certificates andSIGNANDENCRYPTare mandatory. - open62541 does not provide asynchronous APIs for Windows GUI—all network operations must be performed in a separate thread and then synchronized via
PostMessage. - The
StrDBCP,StrDBINI, andStrDBVALstructures form an abstraction layer over OPC UA, allowing switching between different controllers without changing the data-handling logic. - Supporting Siemens S7-1200/S7-1500 requires enabling the OPC UA server in the controller firmware and properly configuring User Access Control (UAC) in TIA Portal.
— Editorial Team
No comments yet.