资讯动态

工业数据采集实战:用C语言和open62541库快速搭建OPC UA客户端(附完整源码)

发布时间:2026/8/23 10:11:58 来源:尧图企业网站定制
工业数据采集实战用C语言和open62541库快速搭建OPC UA客户端附完整源码在工业自动化领域设备间的数据互通一直是工程师们面临的挑战。想象一下当你需要从车间里十几台不同厂商的PLC、传感器和控制器中实时采集温度、压力、转速等关键参数时传统的方式往往需要为每种设备编写特定的驱动和接口这不仅耗时费力还难以维护。而OPC UA协议的出现就像为工业设备间的通信建立了一套通用语言。open62541作为一款开源的OPC UA实现库以其轻量级和跨平台特性成为嵌入式系统和资源受限环境下的理想选择。本文将带你从零开始用C语言和open62541构建一个功能完整的OPC UA客户端实现多节点数据的批量读写和查询。不同于理论介绍我们会聚焦在实际开发中遇到的坑和解决方案并提供可直接用于生产环境的代码示例。1. 环境准备与库配置1.1 获取和编译open62541首先需要从GitHub获取最新版本的open62541源代码git clone https://github.com/open62541/open62541.git cd open62541 mkdir build cd build在Linux环境下使用以下命令编译并安装cmake -DUA_ENABLE_AMALGAMATIONON .. make -j$(nproc) sudo make install对于Windows平台可以使用MinGW或Visual Studio进行编译。需要注意的是Windows下需要额外链接以下库ws2_32.lib (Windows Socket)wsock32.lib (Windows Socket扩展)Iphlpapi.lib (IP帮助API)1.2 项目结构规划建议采用以下目录结构组织你的OPC UA客户端项目opcua_client/ ├── bin/ # 可执行文件输出目录 ├── build/ # 构建目录 ├── src/ │ ├── main.c # 主程序文件 │ ├── opcua_utils.c # 工具函数 │ └── opcua_utils.h └── CMakeLists.txt # 构建配置文件1.3 跨平台编译配置在CMakeLists.txt中我们需要针对不同平台进行条件编译设置# 检测操作系统类型 if(UNIX) set(PLATFORM_LIBS pthread) add_definitions(-DUA_ARCHITECTURE_POSIX) elseif(WIN32) set(PLATFORM_LIBS ws2_32 wsock32 Iphlpapi) add_definitions(-DUA_ARCHITECTURE_WIN32) endif() # 添加可执行文件 add_executable(opcua_client src/main.c src/opcua_utils.c) # 链接库 target_link_libraries(opcua_client ${PLATFORM_LIBS} open62541 )2. 客户端基础架构实现2.1 建立OPC UA连接创建一个稳定的OPC UA客户端连接是第一步。以下代码展示了如何初始化客户端并连接到服务器#include open62541/client.h #include open62541/client_config_default.h UA_Client* create_opcua_client(const char* endpoint_url) { UA_Client *client UA_Client_new(); UA_ClientConfig_setDefault(UA_Client_getConfig(client)); // 设置连接超时为2秒 UA_ClientConfig *config UA_Client_getConfig(client); config-timeout 2000; UA_StatusCode retval UA_Client_connect(client, endpoint_url); if(retval ! UA_STATUSCODE_GOOD) { UA_Client_delete(client); fprintf(stderr, 连接失败: %s\n, UA_StatusCode_name(retval)); return NULL; } printf(成功连接到OPC UA服务器: %s\n, endpoint_url); return client; }2.2 连接状态监测在实际工业环境中网络不稳定是常见问题。我们需要实现连接状态的监测和自动重连机制void check_connection_status(UA_Client *client) { UA_SecureChannelState channelState; UA_SessionState sessionState; UA_StatusCode connectStatus; UA_Client_getState(client, channelState, sessionState, connectStatus); if(channelState ! UA_SECURECHANNELSTATE_OPEN || sessionState ! UA_SESSIONSTATE_ACTIVATED) { printf(连接异常尝试重新连接...\n); UA_Client_disconnect(client); UA_Client_connect(client, opc.tcp://your-server-address:4840); } }2.3 安全配置可选对于需要安全认证的场景可以配置用户名密码或证书void configure_client_security(UA_Client *client, const char* username, const char* password) { UA_ClientConfig *config UA_Client_getConfig(client); config-securityMode UA_MESSAGESECURITYMODE_SIGNANDENCRYPT; // 设置用户名密码认证 if(username password) { UA_UserNameIdentityToken identityToken; UA_UserNameIdentityToken_init(identityToken); identityToken.userName UA_STRING((char*)username); identityToken.password UA_STRING((char*)password); identityToken.policyId UA_STRING_NULL; config-securityPolicyUri UA_STRING(http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256); config-userIdentityToken (UA_UserIdentityToken*)identityToken; } }3. 节点数据操作实战3.1 单节点读写操作最基本的操作是读取单个节点的值。以下函数展示了如何读取并解析节点数据UA_StatusCode read_single_node(UA_Client *client, const char* node_id_str, UA_Int32 *out_value) { UA_NodeId nodeId UA_NODEID_STRING(1, node_id_str); UA_Variant value; UA_StatusCode retval UA_Client_readValueAttribute(client, nodeId, value); if(retval ! UA_STATUSCODE_GOOD) { fprintf(stderr, 读取节点 %s 失败: %s\n, node_id_str, UA_StatusCode_name(retval)); return retval; } if(value.type UA_TYPES[UA_TYPES_INT32]) { *out_value *(UA_Int32*)value.data; printf(节点 %s 的值: %d\n, node_id_str, *out_value); } else { fprintf(stderr, 不支持的节点数据类型\n); retval UA_STATUSCODE_BADTYPEMISMATCH; } UA_Variant_clear(value); return retval; }写入节点值同样重要特别是需要远程控制设备参数时UA_StatusCode write_single_node(UA_Client *client, const char* node_id_str, UA_Int32 new_value) { UA_NodeId nodeId UA_NODEID_STRING(1, node_id_str); UA_Variant value; UA_Variant_setScalar(value, new_value, UA_TYPES[UA_TYPES_INT32]); UA_StatusCode retval UA_Client_writeValueAttribute(client, nodeId, value); if(retval ! UA_STATUSCODE_GOOD) { fprintf(stderr, 写入节点 %s 失败: %s\n, node_id_str, UA_StatusCode_name(retval)); } else { printf(成功写入节点 %s 新值: %d\n, node_id_str, new_value); } return retval; }3.2 多节点批量操作在工业场景中我们经常需要同时读取多个节点的数据。批量操作可以显著提高效率typedef struct { const char* node_id; UA_Int32 value; } NodeValuePair; UA_StatusCode read_multiple_nodes(UA_Client *client, NodeValuePair *nodes, size_t count) { UA_ReadRequest request; UA_ReadRequest_init(request); request.nodesToRead (UA_ReadValueId*)UA_Array_new(count, UA_TYPES[UA_TYPES_READVALUEID]); request.nodesToReadSize count; // 设置要读取的节点列表 for(size_t i 0; i count; i) { request.nodesToRead[i].nodeId UA_NODEID_STRING(1, (char*)nodes[i].node_id); request.nodesToRead[i].attributeId UA_ATTRIBUTEID_VALUE; } UA_ReadResponse response UA_Client_Service_read(client, request); UA_StatusCode overall_status response.responseHeader.serviceResult; if(overall_status UA_STATUSCODE_GOOD) { for(size_t i 0; i response.resultsSize; i) { if(response.results[i].status UA_STATUSCODE_GOOD response.results[i].value.type UA_TYPES[UA_TYPES_INT32]) { nodes[i].value *(UA_Int32*)response.results[i].value.data; printf(节点 %s 的值: %d\n, nodes[i].node_id, nodes[i].value); } else { fprintf(stderr, 读取节点 %s 失败: %s\n, nodes[i].node_id, UA_StatusCode_name(response.results[i].status)); overall_status response.results[i].status; } } } UA_ReadResponse_clear(response); UA_ReadRequest_clear(request); return overall_status; }批量写入多个节点值的实现UA_StatusCode write_multiple_nodes(UA_Client *client, NodeValuePair *nodes, size_t count) { UA_WriteRequest request; UA_WriteRequest_init(request); request.nodesToWrite (UA_WriteValue*)UA_Array_new(count, UA_TYPES[UA_TYPES_WRITEVALUE]); request.nodesToWriteSize count; // 准备要写入的数据 for(size_t i 0; i count; i) { request.nodesToWrite[i].nodeId UA_NODEID_STRING(1, (char*)nodes[i].node_id); request.nodesToWrite[i].attributeId UA_ATTRIBUTEID_VALUE; UA_Variant_setScalar(request.nodesToWrite[i].value.value, nodes[i].value, UA_TYPES[UA_TYPES_INT32]); request.nodesToWrite[i].value.hasValue UA_TRUE; } UA_WriteResponse response UA_Client_Service_write(client, request); UA_StatusCode overall_status response.responseHeader.serviceResult; if(overall_status UA_STATUSCODE_GOOD) { for(size_t i 0; i response.resultsSize; i) { if(response.results[i] ! UA_STATUSCODE_GOOD) { fprintf(stderr, 写入节点 %s 失败: %s\n, nodes[i].node_id, UA_StatusCode_name(response.results[i])); overall_status response.results[i]; } } } if(overall_status UA_STATUSCODE_GOOD) { printf(成功批量写入 %zu 个节点值\n, count); } UA_WriteResponse_clear(response); UA_WriteRequest_clear(request); return overall_status; }3.3 节点浏览与发现在不知道确切节点结构的情况下浏览节点功能非常有用void browse_nodes(UA_Client *client, UA_NodeId starting_node) { UA_BrowseRequest bReq; UA_BrowseRequest_init(bReq); bReq.requestedMaxReferencesPerNode 0; // 0表示不限制 bReq.nodesToBrowse UA_BrowseDescription_new(); bReq.nodesToBrowseSize 1; bReq.nodesToBrowse[0].nodeId starting_node; bReq.nodesToBrowse[0].browseDirection UA_BROWSEDIRECTION_FORWARD; bReq.nodesToBrowse[0].includeSubtypes UA_TRUE; bReq.nodesToBrowse[0].resultMask UA_BROWSERESULTMASK_ALL; UA_BrowseResponse bResp UA_Client_Service_browse(client, bReq); printf(\n浏览节点结果 (从节点NS%u,ID%d开始):\n, starting_node.namespaceIndex, starting_node.identifier.numeric); printf(%-10s %-20s %-30s %-20s\n, 命名空间, 节点ID, 浏览名称, 显示名称); for(size_t i 0; i bResp.resultsSize; i) { for(size_t j 0; j bResp.results[i].referencesSize; j) { UA_ReferenceDescription *ref (bResp.results[i].references[j]); if(ref-nodeId.nodeId.identifierType UA_NODEIDTYPE_NUMERIC) { printf(%-10u %-20d %-30.*s %-20.*s\n, ref-nodeId.nodeId.namespaceIndex, ref-nodeId.nodeId.identifier.numeric, (int)ref-browseName.name.length, ref-browseName.name.data, (int)ref-displayName.text.length, ref-displayName.text.data); } else if(ref-nodeId.nodeId.identifierType UA_NODEIDTYPE_STRING) { printf(%-10u %-20.*s %-30.*s %-20.*s\n, ref-nodeId.nodeId.namespaceIndex, (int)ref-nodeId.nodeId.identifier.string.length, ref-nodeId.nodeId.identifier.string.data, (int)ref-browseName.name.length, ref-browseName.name.data, (int)ref-displayName.text.length, ref-displayName.text.data); } } } UA_BrowseResponse_clear(bResp); }4. 高级功能与性能优化4.1 订阅与数据变化通知比起轮询订阅机制能更高效地获取数据变化UA_StatusCode create_monitored_item(UA_Client *client, const char* node_id_str, UA_UInt32 sampling_interval, UA_UInt32 queue_size) { UA_NodeId nodeId UA_NODEID_STRING(1, node_id_str); UA_MonitoredItemCreateRequest monRequest; UA_MonitoredItemCreateRequest_init(monRequest); monRequest.itemToMonitor.nodeId nodeId; monRequest.itemToMonitor.attributeId UA_ATTRIBUTEID_VALUE; monRequest.monitoringMode UA_MONITORINGMODE_REPORTING; monRequest.requestedParameters.samplingInterval sampling_interval; monRequest.requestedParameters.queueSize queue_size; monRequest.requestedParameters.discardOldest UA_TRUE; UA_CreateSubscriptionRequest subRequest; UA_CreateSubscriptionRequest_init(subRequest); subRequest.requestedPublishingInterval 1000.0; subRequest.requestedLifetimeCount 10000; subRequest.requestedMaxKeepAliveCount 1000; subRequest.maxNotificationsPerPublish 1000; subRequest.publishingEnabled UA_TRUE; subRequest.priority 100; UA_CreateSubscriptionResponse subResponse UA_Client_Subscriptions_create(client, subRequest, NULL, NULL, NULL); if(subResponse.responseHeader.serviceResult ! UA_STATUSCODE_GOOD) { fprintf(stderr, 创建订阅失败: %s\n, UA_StatusCode_name(subResponse.responseHeader.serviceResult)); return subResponse.responseHeader.serviceResult; } UA_UInt32 subId subResponse.subscriptionId; UA_MonitoredItemCreateResult monResponse UA_Client_MonitoredItems_createDataChange(client, subId, UA_TIMESTAMPSTORETURN_BOTH, monRequest, NULL, dataChangeNotificationCallback, NULL); if(monResponse.statusCode ! UA_STATUSCODE_GOOD) { fprintf(stderr, 创建监控项失败: %s\n, UA_StatusCode_name(monResponse.statusCode)); return monResponse.statusCode; } printf(成功为节点 %s 创建监控项订阅ID: %u, 监控项ID: %u\n, node_id_str, subId, monResponse.monitoredItemId); return UA_STATUSCODE_GOOD; } void dataChangeNotificationCallback(UA_Client *client, UA_UInt32 subId, void *subContext, UA_UInt32 monId, void *monContext, UA_DataValue *value) { printf(\n收到数据变化通知 - 订阅ID: %u, 监控项ID: %u\n, subId, monId); if(value-hasValue value-value.type UA_TYPES[UA_TYPES_INT32]) { UA_Int32 currentValue *(UA_Int32*)value-value.data; printf(新值: %d, 源时间戳: %lld, 服务器时间戳: %lld\n, currentValue, value-sourceTimestamp, value-serverTimestamp); } }4.2 异步操作实现同步调用会阻塞线程异步操作能提高程序响应性typedef struct { UA_Client *client; const char* node_id; UA_Int32 value; } AsyncWriteContext; void async_write_callback(UA_Client *client, void *userdata, UA_UInt32 requestId, void *response) { AsyncWriteContext *ctx (AsyncWriteContext*)userdata; UA_WriteResponse *wr (UA_WriteResponse*)response; if(wr-responseHeader.serviceResult UA_STATUSCODE_GOOD) { printf(异步写入节点 %s 成功\n, ctx-node_id); } else { fprintf(stderr, 异步写入节点 %s 失败: %s\n, ctx-node_id, UA_StatusCode_name(wr-responseHeader.serviceResult)); } free(ctx); } UA_StatusCode async_write_node(UA_Client *client, const char* node_id_str, UA_Int32 value) { AsyncWriteContext *ctx malloc(sizeof(AsyncWriteContext)); ctx-client client; ctx-node_id node_id_str; ctx-value value; UA_WriteRequest request; UA_WriteRequest_init(request); request.nodesToWrite UA_WriteValue_new(); request.nodesToWriteSize 1; UA_NodeId nodeId UA_NODEID_STRING(1, (char*)node_id_str); request.nodesToWrite[0].nodeId nodeId; request.nodesToWrite[0].attributeId UA_ATTRIBUTEID_VALUE; UA_Variant_setScalar(request.nodesToWrite[0].value.value, value, UA_TYPES[UA_TYPES_INT32]); request.nodesToWrite[0].value.hasValue UA_TRUE; UA_UInt32 requestId 0; UA_StatusCode retval UA_Client_sendAsyncWriteRequest(client, request, requestId, async_write_callback, ctx); if(retval ! UA_STATUSCODE_GOOD) { free(ctx); fprintf(stderr, 发送异步写入请求失败: %s\n, UA_StatusCode_name(retval)); } else { printf(已发送异步写入请求请求ID: %u\n, requestId); } UA_WriteRequest_clear(request); return retval; }4.3 性能优化技巧批量操作优化将多个读写操作合并到一个请求中合理设置批处理大小通常50-100个节点/批连接池管理复用客户端连接而非频繁创建销毁实现连接池管理多个客户端实例缓存策略对不常变化的数据实施本地缓存设置合理的订阅采样间隔错误处理优化实现指数退避重试机制对暂时性错误自动恢复// 连接池示例结构 typedef struct { UA_Client **clients; size_t size; size_t capacity; pthread_mutex_t lock; } ClientPool; ClientPool* create_client_pool(size_t initial_size, const char* endpoint_url) { ClientPool *pool malloc(sizeof(ClientPool)); pool-clients malloc(sizeof(UA_Client*) * initial_size); pool-capacity initial_size; pool-size 0; pthread_mutex_init(pool-lock, NULL); for(size_t i 0; i initial_size; i) { pool-clients[i] create_opcua_client(endpoint_url); if(pool-clients[i]) { pool-size; } } return pool; } UA_Client* acquire_client(ClientPool *pool) { pthread_mutex_lock(pool-lock); if(pool-size 0) { pthread_mutex_unlock(pool-lock); return NULL; } UA_Client *client pool-clients[--pool-size]; pthread_mutex_unlock(pool-lock); return client; } void release_client(ClientPool *pool, UA_Client *client) { pthread_mutex_lock(pool-lock); if(pool-size pool-capacity) { pool-clients[pool-size] client; } else { UA_Client_delete(client); } pthread_mutex_unlock(pool-lock); }5. 实战案例温度监控系统让我们通过一个完整的温度监控系统案例整合前面介绍的各种技术5.1 系统架构设计------------------- ------------------- ------------------- | 温度传感器节点 | | 压力传感器节点 | | 湿度传感器节点 | | (OPC UA Server) | | (OPC UA Server) | | (OPC UA Server) | ------------------- ------------------- ------------------- | | | | | | v v v ----------------------------------------------------------------------- | 数据采集客户端 | | (基于open62541的OPC UA Client) | ----------------------------------------------------------------------- | v ------------------- | 数据存储与分析 | | (数据库/云端) | -------------------5.2 核心代码实现首先定义系统配置typedef struct { const char* node_id; const char* description; UA_Double min_value; UA_Double max_value; UA_Double alarm_threshold; } SensorConfig; SensorConfig sensor_configs[] { {Temperature_1, 车间1温度, -20.0, 80.0, 70.0}, {Temperature_2, 车间2温度, -20.0, 80.0, 70.0}, {Pressure_1, 管道1压力, 0.0, 10.0, 8.5}, {Humidity_1, 仓库湿度, 0.0, 100.0, 85.0} }; #define NUM_SENSORS (sizeof(sensor_configs)/sizeof(SensorConfig))主监控循环实现void monitor_sensors(UA_Client *client) { NodeValuePair sensors[NUM_SENSORS]; // 初始化传感器节点列表 for(size_t i 0; i NUM_SENSORS; i) { sensors[i].node_id sensor_configs[i].node_id; sensors[i].value 0; } while(1) { UA_StatusCode retval read_multiple_nodes(client, sensors, NUM_SENSORS); if(retval UA_STATUSCODE_GOOD) { for(size_t i 0; i NUM_SENSORS; i) { // 检查是否超过阈值 if(sensors[i].value sensor_configs[i].alarm_threshold) { printf([警报] %s (节点 %s) 值 %.2f 超过阈值 %.2f\n, sensor_configs[i].description, sensor_configs[i].node_id, (UA_Double)sensors[i].value, sensor_configs[i].alarm_threshold); } // 记录数据到数据库或文件 log_sensor_data(sensor_configs[i], sensors[i].value); } } else { fprintf(stderr, 读取传感器数据失败: %s\n, UA_StatusCode_name(retval)); } // 每5秒采集一次 sleep(5); } } void log_sensor_data(const SensorConfig *config, UA_Int32 value) { time_t now time(NULL); char timestamp[64]; strftime(timestamp, sizeof(timestamp), %Y-%m-%d %H:%M:%S, localtime(now)); FILE *log_file fopen(sensor_log.csv, a); if(log_file) { fprintf(log_file, %s,%s,%.2f\n, timestamp, config-node_id, (UA_Double)value); fclose(log_file); } }5.3 异常处理与恢复工业环境中网络波动和设备重启是常态需要健壮的异常处理void robust_monitor_loop(const char* endpoint_url) { UA_Client *client NULL; uint32_t retry_count 0; const uint32_t max_retries 5; while(1) { if(!client) { client create_opcua_client(endpoint_url); if(!client) { retry_count; if(retry_count max_retries) { fprintf(stderr, 达到最大重试次数退出监控\n); break; } uint32_t wait_seconds (1 retry_count); // 指数退避 printf(连接失败等待 %u 秒后重试...\n, wait_seconds); sleep(wait_seconds); continue; } retry_count 0; printf(成功建立连接开始监控传感器\n); } // 检查连接状态 UA_SecureChannelState channelState; UA_SessionState sessionState; UA_StatusCode connectStatus; UA_Client_getState(client, channelState, sessionState, connectStatus); if(channelState ! UA_SECURECHANNELSTATE_OPEN || sessionState ! UA_SESSIONSTATE_ACTIVATED) { fprintf(stderr, 连接异常尝试重新连接...\n); UA_Client_delete(client); client NULL; continue; } // 执行监控循环 monitor_sensors(client); } if(client) { UA_Client_delete(client); } }5.4 部署与运行编译并运行监控客户端# 编译 mkdir build cd build cmake .. make # 运行 (替换为你的OPC UA服务器地址) ./opcua_client opc.tcp://your-server-address:4840系统将开始定期采集各传感器数据并在值超过阈值时发出警报同时将所有数据记录到CSV文件中。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价