跳到主要内容

DataChange 与状态回调 (C)

C SDK 把数据变化和订阅状态分别暴露为两个回调签名:

回调注册函数触发时机
DarraUa_OnDataChange_AddNode / _AddNodes / _MonitoredItem_SetCallbackMI 数据变化
DarraUa_OnSubscriptionState_Subscription_SetStateCallback订阅级状态变化 (LATE / EXPIRED / ...)
关联

DataChange 回调

typedef void (*DarraUa_OnDataChange)(
DarraUa_SubscriptionHandle sub,
DarraUa_MonitoredItemHandle mi,
const DarraUa_DataValue* value,
void* user_context);

参数说明

  • sub — 触发的订阅句柄
  • mi — 触发的 MonitoredItem 本地句柄
  • value — DataValue, 生命周期 = 回调函数内, 不要持久保存指针
  • user_context — 注册时传入的上下文指针

用法

static void on_change(DarraUa_SubscriptionHandle sub,
DarraUa_MonitoredItemHandle mi,
const DarraUa_DataValue* dv,
void* ctx)
{
if (!dv) return;

uint32_t status = DarraUa_DataValue_GetStatus(dv);
DarraUa_DateTime ts = DarraUa_DataValue_GetSourceTimestamp(dv);
const DarraUa_Variant* v = DarraUa_DataValue_GetValue(dv);
DarraUa_BuiltinType type = v ? DarraUa_Variant_GetType(v) : UA_TYPE_NULL;

if (UA_STATUS_IS_BAD(status)) {
printf("[mi=%u] BAD: %s\n", (unsigned)mi, DarraUa_StatusName(status));
return;
}

if (type == UA_TYPE_DOUBLE) {
double d = 0.0;
DarraUa_Variant_GetDouble(v, &d);
printf("[mi=%u] %.3f @ ft=%lld\n", (unsigned)mi, d, (long long)ts);
}
}

user_context 用法

把每个 MI 的元数据 (NodeId 字符串 / 业务对象指针) 通过 user_context 传入, 回调里按 mi 反查:

typedef struct { const char* node_id; int channel_index; } MiContext;

static MiContext g_ctx[100];

for (int i = 0; i < 100; ++i) {
g_ctx[i].node_id = "ns=2;s=...";
g_ctx[i].channel_index = i;
DarraUa_Subscription_AddNode(sub, g_ctx[i].node_id, UA_ATTR_VALUE,
-1.0, on_change, &g_ctx[i], NULL);
}

static void on_change(DarraUa_SubscriptionHandle sub,
DarraUa_MonitoredItemHandle mi,
const DarraUa_DataValue* dv, void* ctx)
{
MiContext* mc = (MiContext*)ctx;
/* 按 mc->channel_index 找业务通道 */
}

订阅级状态回调

typedef enum {
UA_SUB_STATE_GOOD = 0,
UA_SUB_STATE_LATE = 1,
UA_SUB_STATE_KEEP_ALIVE = 2,
UA_SUB_STATE_LIFETIME_EXPIRED = 3,
UA_SUB_STATE_RECREATING = 4
} DarraUa_SubscriptionState;

typedef void (*DarraUa_OnSubscriptionState)(
DarraUa_SubscriptionHandle sub,
DarraUa_SubscriptionState new_state,
DarraUa_Status status,
void* user_context);

OPCUA_API DarraUa_Status OPCUA_CALL
DarraUa_Subscription_SetStateCallback(
DarraUa_SubscriptionHandle sub,
DarraUa_OnSubscriptionState cb,
void* user_context);

状态含义

  • GOOD — 正常, 数据持续推送
  • LATE — 服务端发现 PublishRequest 跟不上 publishingInterval, 数据可能延迟
  • KEEP_ALIVE — 周期 KeepAlive (无数据变化)
  • LIFETIME_EXPIRED — lifetime_count 内未收到 PublishRequest, 服务端清理订阅
  • RECREATING — SDK 检测到订阅丢失, 自动重建中

用法

static void on_sub_state(DarraUa_SubscriptionHandle sub,
DarraUa_SubscriptionState s,
DarraUa_Status code, void* ctx)
{
(void)sub; (void)ctx;
if (s == UA_SUB_STATE_LIFETIME_EXPIRED)
fprintf(stderr, "Subscription expired (%s)\n", DarraUa_StatusName(code));
}

DarraUa_Subscription_SetStateCallback(sub, on_sub_state, NULL);

线程模型

必须切回 UI 线程

回调在 Stack 内部 Publish 线程 上调用, 不在调用方线程. UI 操作必须 marshal 到 UI 线程, 否则跨线程访问控件会崩.

  • 同一订阅的 DataChange 回调串行 (不并发)
  • 跨订阅的回调可能并发, 共享数据加锁
  • 回调里不要做长操作 (>100 ms), 否则阻塞 Publish 队列
  • 回调里若想再次 Read 同节点, 用 DarraUa_Session_ReadNode 同步重读 (不要拷贝 value 指针出去, 它马上失效)

跨平台原子计数:

#if defined(_WIN32)
# include <intrin.h>
# define ATOMIC_INC(p) _InterlockedIncrement((volatile long*)(p))
#else
# define ATOMIC_INC(p) __sync_add_and_fetch((p), 1)
#endif

static volatile long g_callback_count = 0;
static void on_change(...) { ATOMIC_INC(&g_callback_count); }

与 ua.Events.* (C# 概念) 的对比

C# 提供 ua.Events.DataChange 全局聚合通道. C SDK 没有此设施, 业务自己维护一个全局回调 (即 single_cb_AddNodes 里共用) 起到等价作用. 也可在每个 MI 的回调里用 user_context 做"统一 Sink + 业务分发".

下一步