1 | // Copyright (C) 2017 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qopen62541backend.h" |
5 | #include "qopen62541client.h" |
6 | #include "qopen62541node.h" |
7 | #include "qopen62541subscription.h" |
8 | #include "qopen62541utils.h" |
9 | #include "qopen62541valueconverter.h" |
10 | #include "qopen62541utils.h" |
11 | #include <private/qopcuanode_p.h> |
12 | |
13 | #include "qopcuacontentfilterelementresult.h" |
14 | |
15 | #include <QtCore/qloggingcategory.h> |
16 | |
17 | QT_BEGIN_NAMESPACE |
18 | |
19 | Q_DECLARE_LOGGING_CATEGORY(QT_OPCUA_PLUGINS_OPEN62541) |
20 | |
21 | static void monitoredValueHandler(UA_Client *client, UA_UInt32 subId, void *subContext, UA_UInt32 monId, void *monContext, UA_DataValue *value) |
22 | { |
23 | Q_UNUSED(client) |
24 | Q_UNUSED(subId) |
25 | Q_UNUSED(subContext) |
26 | QOpen62541Subscription *subscription = static_cast<QOpen62541Subscription *>(monContext); |
27 | subscription->monitoredValueUpdated(monId, value); |
28 | } |
29 | |
30 | static void stateChangeHandler(UA_Client *client, UA_UInt32 subId, void *subContext, UA_StatusChangeNotification *notification) |
31 | { |
32 | Q_UNUSED(client); |
33 | Q_UNUSED(subId); |
34 | |
35 | if (notification->status != UA_STATUSCODE_BADTIMEOUT) |
36 | return; |
37 | |
38 | QOpen62541Subscription *sub = static_cast<QOpen62541Subscription *>(subContext); |
39 | sub->sendTimeoutNotification(); |
40 | } |
41 | |
42 | static void eventHandler(UA_Client *client, UA_UInt32 subId, void *subContext, UA_UInt32 monId, void *monContext, |
43 | size_t numFields, UA_Variant *eventFields) |
44 | { |
45 | Q_UNUSED(client); |
46 | Q_UNUSED(subId); |
47 | Q_UNUSED(subContext); |
48 | |
49 | QOpen62541Subscription *subscription = static_cast<QOpen62541Subscription *>(monContext); |
50 | |
51 | QVariantList list; |
52 | for (size_t i = 0; i < numFields; ++i) |
53 | list.append(t: QOpen62541ValueConverter::toQVariant(eventFields[i])); |
54 | subscription->eventReceived(monId, list); |
55 | } |
56 | |
57 | QOpen62541Subscription::QOpen62541Subscription(Open62541AsyncBackend *backend, const QOpcUaMonitoringParameters &settings) |
58 | : m_backend(backend) |
59 | , m_interval(settings.publishingInterval()) |
60 | , m_subscriptionId(0) |
61 | , m_lifetimeCount(settings.lifetimeCount() ? settings.lifetimeCount() : UA_CreateSubscriptionRequest_default().requestedLifetimeCount) |
62 | , m_maxKeepaliveCount(settings.maxKeepAliveCount() ? settings.maxKeepAliveCount() : UA_CreateSubscriptionRequest_default().requestedMaxKeepAliveCount) |
63 | , m_shared(settings.subscriptionType()) |
64 | , m_priority(settings.priority()) |
65 | , m_maxNotificationsPerPublish(settings.maxNotificationsPerPublish()) |
66 | , m_clientHandle(0) |
67 | , m_timeout(false) |
68 | { |
69 | } |
70 | |
71 | QOpen62541Subscription::~QOpen62541Subscription() |
72 | { |
73 | removeOnServer(); |
74 | } |
75 | |
76 | UA_UInt32 QOpen62541Subscription::createOnServer() |
77 | { |
78 | UA_CreateSubscriptionRequest req = UA_CreateSubscriptionRequest_default(); |
79 | req.requestedPublishingInterval = m_interval; |
80 | req.requestedLifetimeCount = m_lifetimeCount; |
81 | req.requestedMaxKeepAliveCount = m_maxKeepaliveCount; |
82 | req.priority = m_priority; |
83 | req.maxNotificationsPerPublish = m_maxNotificationsPerPublish; |
84 | UA_CreateSubscriptionResponse res = UA_Client_Subscriptions_create(client: m_backend->m_uaclient, request: req, subscriptionContext: this, statusChangeCallback: stateChangeHandler, deleteCallback: nullptr); |
85 | |
86 | if (res.responseHeader.serviceResult != UA_STATUSCODE_GOOD) { |
87 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not create subscription with interval" << m_interval << UA_StatusCode_name(code: res.responseHeader.serviceResult); |
88 | return 0; |
89 | } |
90 | |
91 | m_subscriptionId = res.subscriptionId; |
92 | m_maxKeepaliveCount = res.revisedMaxKeepAliveCount; |
93 | m_lifetimeCount = res.revisedLifetimeCount; |
94 | m_interval = res.revisedPublishingInterval; |
95 | return m_subscriptionId; |
96 | } |
97 | |
98 | bool QOpen62541Subscription::removeOnServer() |
99 | { |
100 | UA_StatusCode res = UA_STATUSCODE_GOOD; |
101 | if (m_subscriptionId) { |
102 | res = UA_Client_Subscriptions_deleteSingle(client: m_backend->m_uaclient, subscriptionId: m_subscriptionId); |
103 | m_subscriptionId = 0; |
104 | } |
105 | |
106 | for (auto it : std::as_const(t&: m_itemIdToItemMapping)) { |
107 | QOpcUaMonitoringParameters s; |
108 | s.setStatusCode(m_timeout ? QOpcUa::UaStatusCode::BadTimeout : QOpcUa::UaStatusCode::BadDisconnect); |
109 | emit m_backend->monitoringEnableDisable(handle: it->handle, attr: it->attr, subscribe: false, status: s); |
110 | } |
111 | |
112 | qDeleteAll(c: m_itemIdToItemMapping); |
113 | |
114 | m_itemIdToItemMapping.clear(); |
115 | m_nodeHandleToItemMapping.clear(); |
116 | |
117 | return (res == UA_STATUSCODE_GOOD) ? true : false; |
118 | } |
119 | |
120 | void QOpen62541Subscription::modifyMonitoring(quint64 handle, QOpcUa::NodeAttribute attr, QOpcUaMonitoringParameters::Parameter item, QVariant value) |
121 | { |
122 | MonitoredItem *monItem = getItemForAttribute(nodeHandle: handle, attr); |
123 | if (!monItem) { |
124 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify parameter" << item << "there are no monitored items" ; |
125 | QOpcUaMonitoringParameters p; |
126 | p.setStatusCode(QOpcUa::UaStatusCode::BadAttributeIdInvalid); |
127 | emit m_backend->monitoringStatusChanged(handle, attr, items: item, param: p); |
128 | return; |
129 | } |
130 | |
131 | QOpcUaMonitoringParameters p = monItem->parameters; |
132 | p.setStatusCode(QOpcUa::UaStatusCode::BadNotImplemented); |
133 | |
134 | // SetPublishingMode service |
135 | if (item == QOpcUaMonitoringParameters::Parameter::PublishingEnabled) { |
136 | if (value.metaType().id() != QMetaType::Bool) { |
137 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "New value for PublishingEnabled is not a boolean" ; |
138 | p.setStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch); |
139 | emit m_backend->monitoringStatusChanged(handle, attr, items: item, param: p); |
140 | return; |
141 | } |
142 | |
143 | UA_SetPublishingModeRequest req; |
144 | UA_SetPublishingModeRequest_init(p: &req); |
145 | UaDeleter<UA_SetPublishingModeRequest> requestDeleter(&req, UA_SetPublishingModeRequest_clear); |
146 | req.publishingEnabled = value.toBool(); |
147 | req.subscriptionIdsSize = 1; |
148 | req.subscriptionIds = UA_UInt32_new(); |
149 | *req.subscriptionIds = m_subscriptionId; |
150 | UA_SetPublishingModeResponse res = UA_Client_Subscriptions_setPublishingMode(client: m_backend->m_uaclient, request: req); |
151 | UaDeleter<UA_SetPublishingModeResponse> responseDeleter(&res, UA_SetPublishingModeResponse_clear); |
152 | |
153 | if (res.responseHeader.serviceResult != UA_STATUSCODE_GOOD) { |
154 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to set publishing mode:" << res.responseHeader.serviceResult; |
155 | p.setStatusCode(static_cast<QOpcUa::UaStatusCode>(res.responseHeader.serviceResult)); |
156 | emit m_backend->monitoringStatusChanged(handle, attr, items: item, param: p); |
157 | return; |
158 | } |
159 | |
160 | if (res.resultsSize && res.results[0] == UA_STATUSCODE_GOOD) |
161 | p.setPublishingEnabled(value.toBool()); |
162 | |
163 | p.setStatusCode(static_cast<QOpcUa::UaStatusCode>(res.results[0])); |
164 | emit m_backend->monitoringStatusChanged(handle, attr, items: item, param: p); |
165 | |
166 | return; |
167 | } |
168 | |
169 | // SetMonitoringMode service |
170 | if (item == QOpcUaMonitoringParameters::Parameter::MonitoringMode) { |
171 | if (value.userType() != QMetaType::fromType<QOpcUaMonitoringParameters::MonitoringMode>().id()) { |
172 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "New value for MonitoringMode is not a monitoring mode" ; |
173 | p.setStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch); |
174 | emit m_backend->monitoringStatusChanged(handle, attr, items: item, param: p); |
175 | return; |
176 | } |
177 | |
178 | UA_SetMonitoringModeRequest req; |
179 | UA_SetMonitoringModeRequest_init(p: &req); |
180 | UaDeleter<UA_SetMonitoringModeRequest> requestDeleter(&req, UA_SetMonitoringModeRequest_clear); |
181 | req.monitoringMode = static_cast<UA_MonitoringMode>(value.value<QOpcUaMonitoringParameters::MonitoringMode>()); |
182 | req.monitoredItemIdsSize = 1; |
183 | req.monitoredItemIds = UA_UInt32_new(); |
184 | *req.monitoredItemIds = monItem->monitoredItemId; |
185 | req.subscriptionId = m_subscriptionId; |
186 | UA_SetMonitoringModeResponse res = UA_Client_MonitoredItems_setMonitoringMode(client: m_backend->m_uaclient, request: req); |
187 | UaDeleter<UA_SetMonitoringModeResponse> responseDeleter(&res, UA_SetMonitoringModeResponse_clear); |
188 | |
189 | if (res.responseHeader.serviceResult != UA_STATUSCODE_GOOD) { |
190 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to set monitoring mode:" << res.responseHeader.serviceResult; |
191 | p.setStatusCode(static_cast<QOpcUa::UaStatusCode>(res.responseHeader.serviceResult)); |
192 | emit m_backend->monitoringStatusChanged(handle, attr, items: item, param: p); |
193 | return; |
194 | } |
195 | |
196 | if (res.resultsSize && res.results[0] == UA_STATUSCODE_GOOD) |
197 | p.setMonitoringMode(value.value<QOpcUaMonitoringParameters::MonitoringMode>()); |
198 | |
199 | p.setStatusCode(static_cast<QOpcUa::UaStatusCode>(res.results[0])); |
200 | emit m_backend->monitoringStatusChanged(handle, attr, items: item, param: p); |
201 | return; |
202 | } |
203 | |
204 | // SetTriggering service |
205 | if (item == QOpcUaMonitoringParameters::Parameter::TriggeredItemIds) { |
206 | if (!value.canConvert<QSet<quint32>>()) { |
207 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify triggering item id, value is not a set of quint32" ; |
208 | p.setStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch); |
209 | emit m_backend->monitoringStatusChanged(handle, attr, items: item, param: p); |
210 | return; |
211 | } |
212 | |
213 | auto triggeredItemIds = value.value<QSet<quint32>>(); |
214 | |
215 | UA_SetTriggeringRequest triggeringReq; |
216 | UA_SetTriggeringRequest_init(p: &triggeringReq); |
217 | triggeringReq.subscriptionId = m_subscriptionId; |
218 | triggeringReq.triggeringItemId = monItem->monitoredItemId; |
219 | |
220 | QList<quint32> itemsToRemove; |
221 | QList<quint32> itemsToAdd; |
222 | |
223 | if (triggeredItemIds.isEmpty() && !monItem->parameters.triggeredItemIds().isEmpty()) { |
224 | itemsToRemove = monItem->parameters.triggeredItemIds().values(); |
225 | } else if (!triggeredItemIds.isEmpty()) { |
226 | itemsToAdd = triggeredItemIds.values(); |
227 | itemsToRemove = monItem->parameters.triggeredItemIds().subtract(other: triggeredItemIds).values(); |
228 | } |
229 | |
230 | if (itemsToAdd.isEmpty() && itemsToRemove.isEmpty()) { |
231 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Nothing to do for TriggeredItemIds" ; |
232 | p.setStatusCode(QOpcUa::UaStatusCode::Good); |
233 | emit m_backend->monitoringStatusChanged(handle, attr, items: item, param: p); |
234 | return; |
235 | } |
236 | |
237 | if (!itemsToAdd.isEmpty()) { |
238 | triggeringReq.linksToAddSize = itemsToAdd.size(); |
239 | triggeringReq.linksToAdd = itemsToAdd.data(); |
240 | } |
241 | |
242 | if (!itemsToRemove.isEmpty()) { |
243 | triggeringReq.linksToRemoveSize = itemsToRemove.size(); |
244 | triggeringReq.linksToRemove = itemsToRemove.data(); |
245 | } |
246 | |
247 | auto triggeringRes = UA_Client_MonitoredItems_setTriggering(client: m_backend->m_uaclient, request: triggeringReq); |
248 | |
249 | QHash<quint32, QOpcUa::UaStatusCode> failedItems; |
250 | |
251 | if (triggeringRes.responseHeader.serviceResult != UA_STATUSCODE_GOOD) { |
252 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Modifying TriggeredItemIds failed with" |
253 | << UA_StatusCode_name(code: triggeringRes.responseHeader.serviceResult); |
254 | |
255 | for (const auto &entry : itemsToAdd) |
256 | failedItems[entry] = QOpcUa::UaStatusCode(triggeringRes.responseHeader.serviceResult); |
257 | p.setFailedTriggeredItemsStatus(failedItems); |
258 | p.setStatusCode(QOpcUa::UaStatusCode(triggeringRes.responseHeader.serviceResult)); |
259 | emit m_backend->monitoringStatusChanged(handle, attr, items: item, param: p); |
260 | UA_SetTriggeringResponse_clear(p: &triggeringRes); |
261 | return; |
262 | } |
263 | |
264 | for (size_t i = 0; i < triggeringRes.addResultsSize; ++i) { |
265 | if (triggeringRes.addResults[i] != UA_STATUSCODE_GOOD) { |
266 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to add trigger link" << triggeringReq.triggeringItemId |
267 | << "->" << itemsToAdd.at(i) << "on subscription" << m_subscriptionId |
268 | << "with status" |
269 | << UA_StatusCode_name(code: triggeringRes.addResults[i]); |
270 | failedItems.insert(key: itemsToAdd.at(i), value: QOpcUa::UaStatusCode(triggeringRes.addResults[i])); |
271 | triggeredItemIds.remove(value: itemsToAdd.at(i)); |
272 | } |
273 | } |
274 | |
275 | UA_SetTriggeringResponse_clear(p: &triggeringRes); |
276 | |
277 | monItem->parameters.setTriggeredItemIds(triggeredItemIds); |
278 | p.setStatusCode(QOpcUa::UaStatusCode::Good); |
279 | p.setTriggeredItemIds(triggeredItemIds); |
280 | p.setFailedTriggeredItemsStatus(failedItems); |
281 | emit m_backend->monitoringStatusChanged(handle, attr, items: item, param: p); |
282 | |
283 | return; |
284 | } |
285 | |
286 | if (modifySubscriptionParameters(nodeHandle: handle, attr, item, value)) |
287 | return; |
288 | if (modifyMonitoredItemParameters(nodeHandle: handle, attr, item, value)) |
289 | return; |
290 | |
291 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Modifying" << item << "is not implemented" ; |
292 | p.setStatusCode(QOpcUa::UaStatusCode::BadNotImplemented); |
293 | emit m_backend->monitoringStatusChanged(handle, attr, items: item, param: p); |
294 | } |
295 | |
296 | bool QOpen62541Subscription::addAttributeMonitoredItem(quint64 handle, QOpcUa::NodeAttribute attr, const UA_NodeId &id, QOpcUaMonitoringParameters settings) |
297 | { |
298 | UA_MonitoredItemCreateRequest req; |
299 | UA_MonitoredItemCreateRequest_init(p: &req); |
300 | UaDeleter<UA_MonitoredItemCreateRequest> requestDeleter(&req, UA_MonitoredItemCreateRequest_clear); |
301 | req.itemToMonitor.attributeId = QOpen62541ValueConverter::toUaAttributeId(attr); |
302 | UA_NodeId_copy(src: &id, dst: &(req.itemToMonitor.nodeId)); |
303 | if (settings.indexRange().size()) |
304 | QOpen62541ValueConverter::scalarFromQt<UA_String, QString>(var: settings.indexRange(), ptr: &req.itemToMonitor.indexRange); |
305 | req.monitoringMode = static_cast<UA_MonitoringMode>(settings.monitoringMode()); |
306 | req.requestedParameters.samplingInterval = qFuzzyCompare(p1: settings.samplingInterval(), p2: 0.0) ? m_interval : settings.samplingInterval(); |
307 | req.requestedParameters.queueSize = settings.queueSize() == 0 ? 1 : settings.queueSize(); |
308 | req.requestedParameters.discardOldest = settings.discardOldest(); |
309 | req.requestedParameters.clientHandle = ++m_clientHandle; |
310 | |
311 | if (settings.filter().isValid()) { |
312 | UA_ExtensionObject filter = createFilter(filterData: settings.filter()); |
313 | if (filter.content.decoded.data) |
314 | req.requestedParameters.filter = filter; |
315 | else { |
316 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not create monitored item, filter creation failed" ; |
317 | QOpcUaMonitoringParameters s; |
318 | s.setStatusCode(QOpcUa::UaStatusCode::BadInternalError); |
319 | emit m_backend->monitoringEnableDisable(handle, attr, subscribe: true, status: s); |
320 | return false; |
321 | } |
322 | } |
323 | |
324 | UA_MonitoredItemCreateResult res; |
325 | UaDeleter<UA_MonitoredItemCreateResult> resultDeleter(&res, UA_MonitoredItemCreateResult_clear); |
326 | |
327 | if (attr == QOpcUa::NodeAttribute::EventNotifier && settings.filter().canConvert<QOpcUaMonitoringParameters::EventFilter>()) |
328 | res = UA_Client_MonitoredItems_createEvent(client: m_backend->m_uaclient, subscriptionId: m_subscriptionId, |
329 | timestampsToReturn: UA_TIMESTAMPSTORETURN_BOTH, item: req, context: this, callback: eventHandler, deleteCallback: nullptr); |
330 | else |
331 | res = UA_Client_MonitoredItems_createDataChange(client: m_backend->m_uaclient, subscriptionId: m_subscriptionId, timestampsToReturn: UA_TIMESTAMPSTORETURN_BOTH, item: req, context: this, callback: monitoredValueHandler, deleteCallback: nullptr); |
332 | |
333 | if (res.statusCode != UA_STATUSCODE_GOOD) { |
334 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not add monitored item for" << attr << "of node" << Open62541Utils::nodeIdToQString(id) << ":" << UA_StatusCode_name(code: res.statusCode); |
335 | QOpcUaMonitoringParameters s; |
336 | s.setStatusCode(static_cast<QOpcUa::UaStatusCode>(res.statusCode)); |
337 | emit m_backend->monitoringEnableDisable(handle, attr, subscribe: true, status: s); |
338 | return false; |
339 | } |
340 | |
341 | QSet<quint32> successfulTriggerLinks; |
342 | QHash<quint32, QOpcUa::UaStatusCode> failedTriggerLinks; |
343 | if (!settings.triggeredItemIds().isEmpty()) { |
344 | auto triggeredItems = settings.triggeredItemIds().values(); |
345 | |
346 | UA_SetTriggeringRequest req; |
347 | UA_SetTriggeringRequest_init(p: &req); |
348 | req.subscriptionId = m_subscriptionId; |
349 | req.triggeringItemId = res.monitoredItemId; |
350 | req.linksToAddSize = triggeredItems.size(); |
351 | req.linksToAdd = triggeredItems.data(); |
352 | |
353 | auto triggeringRes = UA_Client_MonitoredItems_setTriggering(client: m_backend->m_uaclient, request: req); |
354 | |
355 | if (triggeringRes.responseHeader.serviceResult != UA_STATUSCODE_GOOD) { |
356 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not set triggering item it for" << attr << "of node" << Open62541Utils::nodeIdToQString(id) << ":" |
357 | << UA_StatusCode_name(code: triggeringRes.responseHeader.serviceResult); |
358 | |
359 | // Remove the new monitored item |
360 | UA_DeleteMonitoredItemsRequest deleteRequest; |
361 | UA_DeleteMonitoredItemsRequest_init(p: &deleteRequest); |
362 | deleteRequest.subscriptionId = m_subscriptionId; |
363 | deleteRequest.monitoredItemIdsSize = 1; |
364 | deleteRequest.monitoredItemIds = &res.monitoredItemId; |
365 | UA_Client_MonitoredItems_delete(client: m_backend->m_uaclient, deleteRequest); |
366 | |
367 | for (const auto &entry : triggeredItems) |
368 | failedTriggerLinks.insert(key: entry, value: QOpcUa::UaStatusCode(triggeringRes.responseHeader.serviceResult)); |
369 | |
370 | QOpcUaMonitoringParameters s; |
371 | s.setStatusCode(static_cast<QOpcUa::UaStatusCode>(triggeringRes.responseHeader.serviceResult)); |
372 | s.setFailedTriggeredItemsStatus(failedTriggerLinks); |
373 | emit m_backend->monitoringEnableDisable(handle, attr, subscribe: true, status: s); |
374 | |
375 | UA_SetTriggeringResponse_clear(p: &triggeringRes); |
376 | |
377 | return false; |
378 | } |
379 | |
380 | for (size_t i = 0; i < triggeringRes.addResultsSize; ++i) { |
381 | if (triggeringRes.addResults[i] == UA_STATUSCODE_GOOD) { |
382 | successfulTriggerLinks.insert(value: triggeredItems.at(i)); |
383 | } else { |
384 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to add trigger link" << res.monitoredItemId |
385 | << "->" << triggeredItems.at(i) << "on subscription" << m_subscriptionId |
386 | << "with status" |
387 | << UA_StatusCode_name(code: triggeringRes.addResults[i]); |
388 | failedTriggerLinks.insert(key: triggeredItems.at(i), value: QOpcUa::UaStatusCode(triggeringRes.addResults[i])); |
389 | } |
390 | } |
391 | |
392 | UA_SetTriggeringResponse_clear(p: &triggeringRes); |
393 | } |
394 | |
395 | MonitoredItem *temp = new MonitoredItem(handle, attr, res.monitoredItemId); |
396 | m_nodeHandleToItemMapping[handle][attr] = temp; |
397 | m_itemIdToItemMapping[res.monitoredItemId] = temp; |
398 | |
399 | QOpcUaMonitoringParameters s = settings; |
400 | s.setSubscriptionId(m_subscriptionId); |
401 | s.setPublishingInterval(m_interval); |
402 | s.setMaxKeepAliveCount(m_maxKeepaliveCount); |
403 | s.setLifetimeCount(m_lifetimeCount); |
404 | s.setStatusCode(QOpcUa::UaStatusCode::Good); |
405 | s.setSamplingInterval(res.revisedSamplingInterval); |
406 | s.setQueueSize(res.revisedQueueSize); |
407 | s.setMonitoredItemId(res.monitoredItemId); |
408 | s.setTriggeredItemIds(successfulTriggerLinks); |
409 | s.setFailedTriggeredItemsStatus(failedTriggerLinks); |
410 | temp->parameters = s; |
411 | temp->clientHandle = m_clientHandle; |
412 | |
413 | if (res.filterResult.encoding >= UA_EXTENSIONOBJECT_DECODED && |
414 | res.filterResult.content.decoded.type == &UA_TYPES[UA_TYPES_EVENTFILTERRESULT]) |
415 | s.setFilterResult(convertEventFilterResult(obj: &res.filterResult)); |
416 | else |
417 | s.clearFilterResult(); |
418 | |
419 | emit m_backend->monitoringEnableDisable(handle, attr, subscribe: true, status: s); |
420 | |
421 | return true; |
422 | } |
423 | |
424 | bool QOpen62541Subscription::removeAttributeMonitoredItem(quint64 handle, QOpcUa::NodeAttribute attr) |
425 | { |
426 | MonitoredItem *item = getItemForAttribute(nodeHandle: handle, attr); |
427 | if (!item) { |
428 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "There is no monitored item for this attribute" ; |
429 | QOpcUaMonitoringParameters s; |
430 | s.setStatusCode(QOpcUa::UaStatusCode::BadMonitoredItemIdInvalid); |
431 | emit m_backend->monitoringEnableDisable(handle, attr, subscribe: false, status: s); |
432 | return false; |
433 | } |
434 | |
435 | UA_StatusCode res = UA_Client_MonitoredItems_deleteSingle(client: m_backend->m_uaclient, subscriptionId: m_subscriptionId, monitoredItemId: item->monitoredItemId); |
436 | if (res != UA_STATUSCODE_GOOD) |
437 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not remove monitored item" << item->monitoredItemId << "from subscription" << m_subscriptionId << ":" << UA_StatusCode_name(code: res); |
438 | |
439 | m_itemIdToItemMapping.remove(key: item->monitoredItemId); |
440 | const auto it = m_nodeHandleToItemMapping.find(key: handle); |
441 | it->remove(key: attr); |
442 | if (it->empty()) |
443 | m_nodeHandleToItemMapping.remove(key: it.key()); |
444 | |
445 | delete item; |
446 | |
447 | QOpcUaMonitoringParameters s; |
448 | s.setStatusCode(static_cast<QOpcUa::UaStatusCode>(res)); |
449 | emit m_backend->monitoringEnableDisable(handle, attr, subscribe: false, status: s); |
450 | |
451 | return true; |
452 | } |
453 | |
454 | void QOpen62541Subscription::monitoredValueUpdated(UA_UInt32 monId, UA_DataValue *value) |
455 | { |
456 | auto item = m_itemIdToItemMapping.constFind(key: monId); |
457 | if (item == m_itemIdToItemMapping.constEnd()) |
458 | return; |
459 | QOpcUaReadResult res; |
460 | |
461 | if (!value || value == UA_EMPTY_ARRAY_SENTINEL) { |
462 | res.setStatusCode(QOpcUa::UaStatusCode::Good); |
463 | emit m_backend->dataChangeOccurred(handle: item.value()->handle, res); |
464 | return; |
465 | } |
466 | |
467 | res.setValue(QOpen62541ValueConverter::toQVariant(value->value)); |
468 | res.setAttribute(item.value()->attr); |
469 | if (value->hasServerTimestamp) |
470 | res.setServerTimestamp(QOpen62541ValueConverter::scalarToQt<QDateTime, UA_DateTime>(data: &value->serverTimestamp)); |
471 | if (value->hasSourceTimestamp) |
472 | res.setSourceTimestamp(QOpen62541ValueConverter::scalarToQt<QDateTime, UA_DateTime>(data: &value->sourceTimestamp)); |
473 | res.setStatusCode(value->hasStatus ? QOpcUa::UaStatusCode(value->status) : QOpcUa::UaStatusCode::Good); |
474 | emit m_backend->dataChangeOccurred(handle: item.value()->handle, res); |
475 | } |
476 | |
477 | void QOpen62541Subscription::sendTimeoutNotification() |
478 | { |
479 | QList<QPair<quint64, QOpcUa::NodeAttribute>> items; |
480 | for (const auto &it : std::as_const(t&: m_nodeHandleToItemMapping)) { |
481 | for (auto item : it) { |
482 | items.push_back(t: {item->handle, item->attr}); |
483 | } |
484 | } |
485 | emit timeout(sub: this, items); |
486 | m_timeout = true; |
487 | } |
488 | |
489 | void QOpen62541Subscription::eventReceived(UA_UInt32 monId, QVariantList list) |
490 | { |
491 | auto item = m_itemIdToItemMapping.constFind(key: monId); |
492 | if (item == m_itemIdToItemMapping.constEnd()) |
493 | return; |
494 | emit m_backend->eventOccurred(handle: item.value()->handle, fields: list); |
495 | } |
496 | |
497 | double QOpen62541Subscription::interval() const |
498 | { |
499 | return m_interval; |
500 | } |
501 | |
502 | UA_UInt32 QOpen62541Subscription::subscriptionId() const |
503 | { |
504 | return m_subscriptionId; |
505 | } |
506 | |
507 | int QOpen62541Subscription::monitoredItemsCount() const |
508 | { |
509 | return m_itemIdToItemMapping.size(); |
510 | } |
511 | |
512 | QOpcUaMonitoringParameters::SubscriptionType QOpen62541Subscription::shared() const |
513 | { |
514 | return m_shared; |
515 | } |
516 | |
517 | QOpen62541Subscription::MonitoredItem *QOpen62541Subscription::getItemForAttribute(quint64 nodeHandle, QOpcUa::NodeAttribute attr) |
518 | { |
519 | auto nodeEntry = m_nodeHandleToItemMapping.constFind(key: nodeHandle); |
520 | |
521 | if (nodeEntry == m_nodeHandleToItemMapping.constEnd()) |
522 | return nullptr; |
523 | |
524 | auto item = nodeEntry->constFind(key: attr); |
525 | if (item == nodeEntry->constEnd()) |
526 | return nullptr; |
527 | |
528 | return item.value(); |
529 | } |
530 | |
531 | UA_ExtensionObject QOpen62541Subscription::createFilter(const QVariant &filterData) |
532 | { |
533 | UA_ExtensionObject obj; |
534 | UA_ExtensionObject_init(p: &obj); |
535 | |
536 | if (filterData.canConvert<QOpcUaMonitoringParameters::DataChangeFilter>()) { |
537 | createDataChangeFilter(filter: filterData.value<QOpcUaMonitoringParameters::DataChangeFilter>(), out: &obj); |
538 | return obj; |
539 | } |
540 | |
541 | if (filterData.canConvert<QOpcUaMonitoringParameters::EventFilter>()) { |
542 | Open62541Utils::createEventFilter(filter: filterData.value<QOpcUaMonitoringParameters::EventFilter>(), out: &obj); |
543 | return obj; |
544 | } |
545 | |
546 | if (filterData.isValid()) |
547 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not create filter, invalid input." ; |
548 | |
549 | return obj; |
550 | } |
551 | |
552 | void QOpen62541Subscription::createDataChangeFilter(const QOpcUaMonitoringParameters::DataChangeFilter &filter, UA_ExtensionObject *out) |
553 | { |
554 | UA_DataChangeFilter *uaFilter = UA_DataChangeFilter_new(); |
555 | uaFilter->deadbandType = static_cast<UA_UInt32>(filter.deadbandType()); |
556 | uaFilter->deadbandValue = filter.deadbandValue(); |
557 | uaFilter->trigger = static_cast<UA_DataChangeTrigger>(filter.trigger()); |
558 | out->encoding = UA_EXTENSIONOBJECT_DECODED; |
559 | out->content.decoded.type = &UA_TYPES[UA_TYPES_DATACHANGEFILTER]; |
560 | out->content.decoded.data = uaFilter; |
561 | } |
562 | |
563 | QOpcUaEventFilterResult QOpen62541Subscription::convertEventFilterResult(UA_ExtensionObject *obj) |
564 | { |
565 | QOpcUaEventFilterResult result; |
566 | |
567 | if (!obj) |
568 | return result; |
569 | |
570 | if (obj->encoding == UA_EXTENSIONOBJECT_DECODED && obj->content.decoded.type == &UA_TYPES[UA_TYPES_EVENTFILTERRESULT]) { |
571 | UA_EventFilterResult *filterResult = static_cast<UA_EventFilterResult *>(obj->content.decoded.data); |
572 | |
573 | for (size_t i = 0; i < filterResult->selectClauseResultsSize; ++i) |
574 | result.selectClauseResultsRef().append(t: static_cast<QOpcUa::UaStatusCode>(filterResult->selectClauseResults[i])); |
575 | |
576 | for (size_t i = 0; i < filterResult->whereClauseResult.elementResultsSize; ++i) { |
577 | QOpcUaContentFilterElementResult temp; |
578 | temp.setStatusCode(static_cast<QOpcUa::UaStatusCode>(filterResult->whereClauseResult.elementResults[i].statusCode)); |
579 | for (size_t j = 0; j < filterResult->whereClauseResult.elementResults[i].operandStatusCodesSize; ++j) |
580 | temp.operandStatusCodesRef().append(t: static_cast<QOpcUa::UaStatusCode>( |
581 | filterResult->whereClauseResult.elementResults[i].operandStatusCodes[j])); |
582 | result.whereClauseResultsRef().append(t: temp); |
583 | } |
584 | } |
585 | |
586 | return result; |
587 | } |
588 | |
589 | bool QOpen62541Subscription::modifySubscriptionParameters(quint64 nodeHandle, QOpcUa::NodeAttribute attr, const QOpcUaMonitoringParameters::Parameter &item, const QVariant &value) |
590 | { |
591 | UA_ModifySubscriptionRequest req; |
592 | UA_ModifySubscriptionRequest_init(p: &req); |
593 | req.subscriptionId = m_subscriptionId; |
594 | req.requestedPublishingInterval = m_interval; |
595 | req.requestedLifetimeCount = m_lifetimeCount; |
596 | req.requestedMaxKeepAliveCount = m_maxKeepaliveCount; |
597 | req.maxNotificationsPerPublish = m_maxNotificationsPerPublish; |
598 | |
599 | bool match = true; |
600 | |
601 | switch (item) { |
602 | case QOpcUaMonitoringParameters::Parameter::PublishingInterval: { |
603 | bool ok; |
604 | req.requestedPublishingInterval = value.toDouble(ok: &ok); |
605 | if (!ok) { |
606 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify PublishingInterval, value is not a double" ; |
607 | QOpcUaMonitoringParameters p; |
608 | p.setStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch); |
609 | emit m_backend->monitoringStatusChanged(handle: nodeHandle, attr, items: item, param: p); |
610 | return true; |
611 | } |
612 | break; |
613 | } |
614 | case QOpcUaMonitoringParameters::Parameter::LifetimeCount: { |
615 | bool ok; |
616 | req.requestedLifetimeCount = value.toUInt(ok: &ok); |
617 | if (!ok) { |
618 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify LifetimeCount, value is not an integer" ; |
619 | QOpcUaMonitoringParameters p; |
620 | p.setStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch); |
621 | emit m_backend->monitoringStatusChanged(handle: nodeHandle, attr, items: item, param: p); |
622 | return true; |
623 | } |
624 | break; |
625 | } |
626 | case QOpcUaMonitoringParameters::Parameter::MaxKeepAliveCount: { |
627 | bool ok; |
628 | req.requestedMaxKeepAliveCount = value.toUInt(ok: &ok); |
629 | if (!ok) { |
630 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify MaxKeepAliveCount, value is not an integer" ; |
631 | QOpcUaMonitoringParameters p; |
632 | p.setStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch); |
633 | emit m_backend->monitoringStatusChanged(handle: nodeHandle, attr, items: item, param: p); |
634 | return true; |
635 | } |
636 | break; |
637 | } |
638 | case QOpcUaMonitoringParameters::Parameter::Priority: { |
639 | bool ok; |
640 | req.priority = value.toUInt(ok: &ok); |
641 | if (!ok) { |
642 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify Priority, value is not an integer" ; |
643 | QOpcUaMonitoringParameters p; |
644 | p.setStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch); |
645 | emit m_backend->monitoringStatusChanged(handle: nodeHandle, attr, items: item, param: p); |
646 | return true; |
647 | } |
648 | break; |
649 | } |
650 | case QOpcUaMonitoringParameters::Parameter::MaxNotificationsPerPublish: { |
651 | bool ok; |
652 | req.maxNotificationsPerPublish = value.toUInt(ok: &ok); |
653 | if (!ok) { |
654 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify MaxNotificationsPerPublish, value is not an integer" ; |
655 | QOpcUaMonitoringParameters p; |
656 | p.setStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch); |
657 | emit m_backend->monitoringStatusChanged(handle: nodeHandle, attr, items: item, param: p); |
658 | return true; |
659 | } |
660 | break; |
661 | } |
662 | default: |
663 | match = false; |
664 | break; |
665 | } |
666 | |
667 | if (match) { |
668 | UA_ModifySubscriptionResponse res = UA_Client_Subscriptions_modify(client: m_backend->m_uaclient, request: req); |
669 | |
670 | if (res.responseHeader.serviceResult != UA_STATUSCODE_GOOD) { |
671 | QOpcUaMonitoringParameters p; |
672 | p.setStatusCode(static_cast<QOpcUa::UaStatusCode>(res.responseHeader.serviceResult)); |
673 | emit m_backend->monitoringStatusChanged(handle: nodeHandle, attr, items: item, param: p); |
674 | } else { |
675 | QOpcUaMonitoringParameters::Parameters changed = item; |
676 | if (!qFuzzyCompare(p1: res.revisedPublishingInterval, p2: m_interval)) |
677 | changed |= QOpcUaMonitoringParameters::Parameter::PublishingInterval; |
678 | if (res.revisedLifetimeCount != m_lifetimeCount) |
679 | changed |= QOpcUaMonitoringParameters::Parameter::LifetimeCount; |
680 | if (res.revisedMaxKeepAliveCount != m_maxKeepaliveCount) |
681 | changed |= QOpcUaMonitoringParameters::Parameter::MaxKeepAliveCount; |
682 | |
683 | m_lifetimeCount = res.revisedLifetimeCount; |
684 | m_maxKeepaliveCount = res.revisedMaxKeepAliveCount; |
685 | m_interval = res.revisedPublishingInterval; |
686 | if (item == QOpcUaMonitoringParameters::Parameter::Priority) |
687 | m_priority = value.toUInt(); |
688 | if (item == QOpcUaMonitoringParameters::Parameter::MaxNotificationsPerPublish) |
689 | m_maxNotificationsPerPublish = value.toUInt(); |
690 | |
691 | QOpcUaMonitoringParameters p; |
692 | p.setStatusCode(QOpcUa::UaStatusCode::Good); |
693 | p.setPublishingInterval(m_interval); |
694 | p.setLifetimeCount(m_lifetimeCount); |
695 | p.setMaxKeepAliveCount(m_maxKeepaliveCount); |
696 | p.setPriority(m_priority); |
697 | p.setMaxNotificationsPerPublish(m_maxNotificationsPerPublish); |
698 | |
699 | for (auto it : std::as_const(t&: m_itemIdToItemMapping)) |
700 | emit m_backend->monitoringStatusChanged(handle: it->handle, attr: it->attr, items: changed, param: p); |
701 | } |
702 | return true; |
703 | } |
704 | return false; |
705 | } |
706 | |
707 | bool QOpen62541Subscription::modifyMonitoredItemParameters(quint64 nodeHandle, QOpcUa::NodeAttribute attr, const QOpcUaMonitoringParameters::Parameter &item, const QVariant &value) |
708 | { |
709 | MonitoredItem *monItem = getItemForAttribute(nodeHandle, attr); |
710 | QOpcUaMonitoringParameters p = monItem->parameters; |
711 | |
712 | UA_ModifyMonitoredItemsRequest req; |
713 | UA_ModifyMonitoredItemsRequest_init(p: &req); |
714 | UaDeleter<UA_ModifyMonitoredItemsRequest> requestDeleter(&req, UA_ModifyMonitoredItemsRequest_clear); |
715 | req.subscriptionId = m_subscriptionId; |
716 | req.itemsToModifySize = 1; |
717 | req.itemsToModify = UA_MonitoredItemModifyRequest_new(); |
718 | UA_MonitoredItemModifyRequest_init(p: req.itemsToModify); |
719 | req.itemsToModify->monitoredItemId = monItem->monitoredItemId; |
720 | req.itemsToModify->requestedParameters.discardOldest = monItem->parameters.discardOldest(); |
721 | req.itemsToModify->requestedParameters.queueSize = monItem->parameters.queueSize(); |
722 | req.itemsToModify->requestedParameters.samplingInterval = monItem->parameters.samplingInterval(); |
723 | req.itemsToModify->monitoredItemId = monItem->monitoredItemId; |
724 | req.itemsToModify->requestedParameters.clientHandle = monItem->clientHandle; |
725 | |
726 | bool match = true; |
727 | |
728 | switch (item) { |
729 | case QOpcUaMonitoringParameters::Parameter::DiscardOldest: { |
730 | if (value.metaType().id() != QMetaType::Bool) { |
731 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify DiscardOldest, value is not a bool" ; |
732 | p.setStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch); |
733 | emit m_backend->monitoringStatusChanged(handle: nodeHandle, attr, items: item, param: p); |
734 | return true; |
735 | } |
736 | req.itemsToModify->requestedParameters.discardOldest = value.toBool(); |
737 | break; |
738 | } |
739 | case QOpcUaMonitoringParameters::Parameter::QueueSize: { |
740 | if (value.metaType().id() != QMetaType::UInt) { |
741 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify QueueSize, value is not an integer" ; |
742 | p.setStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch); |
743 | emit m_backend->monitoringStatusChanged(handle: nodeHandle, attr, items: item, param: p); |
744 | return true; |
745 | } |
746 | req.itemsToModify->requestedParameters.queueSize = value.toUInt(); |
747 | break; |
748 | } |
749 | case QOpcUaMonitoringParameters::Parameter::SamplingInterval: { |
750 | if (value.metaType().id() != QMetaType::Double) { |
751 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify SamplingInterval, value is not a double" ; |
752 | p.setStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch); |
753 | emit m_backend->monitoringStatusChanged(handle: nodeHandle, attr, items: item, param: p); |
754 | return true; |
755 | } |
756 | req.itemsToModify->requestedParameters.samplingInterval = value.toDouble(); |
757 | break; |
758 | } |
759 | case QOpcUaMonitoringParameters::Parameter::Filter: { |
760 | UA_ExtensionObject filter = createFilter(filterData: value); |
761 | if (filter.content.decoded.data) |
762 | req.itemsToModify->requestedParameters.filter = filter; |
763 | else { |
764 | qCDebug(QT_OPCUA_PLUGINS_OPEN62541) << "Unable to modify filter, filter creation failed" ; |
765 | p.setStatusCode(QOpcUa::UaStatusCode::BadInternalError); |
766 | emit m_backend->monitoringStatusChanged(handle: nodeHandle, attr, items: item, param: p); |
767 | return true; |
768 | } |
769 | break; |
770 | } |
771 | default: |
772 | match = false; |
773 | break; |
774 | } |
775 | |
776 | if (match) { |
777 | if (item != QOpcUaMonitoringParameters::Parameter::Filter && p.filter().isValid()) { |
778 | UA_ExtensionObject filter = createFilter(filterData: monItem->parameters.filter()); |
779 | if (filter.content.decoded.data) |
780 | req.itemsToModify->requestedParameters.filter = filter; |
781 | else { |
782 | qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify monitored item, filter creation failed" ; |
783 | p.setStatusCode(QOpcUa::UaStatusCode::BadInternalError); |
784 | emit m_backend->monitoringStatusChanged(handle: nodeHandle, attr, items: item, param: p); |
785 | return true; |
786 | } |
787 | } |
788 | |
789 | UA_ModifyMonitoredItemsResponse res = UA_Client_MonitoredItems_modify(client: m_backend->m_uaclient, request: req); |
790 | UaDeleter<UA_ModifyMonitoredItemsResponse> responseDeleter( |
791 | &res, UA_ModifyMonitoredItemsResponse_clear); |
792 | |
793 | if (res.responseHeader.serviceResult != UA_STATUSCODE_GOOD || res.results[0].statusCode != UA_STATUSCODE_GOOD) { |
794 | p.setStatusCode(static_cast<QOpcUa::UaStatusCode>(res.responseHeader.serviceResult == UA_STATUSCODE_GOOD ? res.results[0].statusCode : res.responseHeader.serviceResult)); |
795 | emit m_backend->monitoringStatusChanged(handle: nodeHandle, attr, items: item, param: p); |
796 | return true; |
797 | } else { |
798 | p.setStatusCode(QOpcUa::UaStatusCode::Good); |
799 | QOpcUaMonitoringParameters::Parameters changed = item; |
800 | if (!qFuzzyCompare(p1: p.samplingInterval(), p2: res.results[0].revisedSamplingInterval)) { |
801 | p.setSamplingInterval(res.results[0].revisedSamplingInterval); |
802 | changed |= QOpcUaMonitoringParameters::Parameter::SamplingInterval; |
803 | } |
804 | if (p.queueSize() != res.results[0].revisedQueueSize) { |
805 | p.setQueueSize(res.results[0].revisedQueueSize); |
806 | changed |= QOpcUaMonitoringParameters::Parameter::QueueSize; |
807 | } |
808 | |
809 | if (item == QOpcUaMonitoringParameters::Parameter::DiscardOldest) { |
810 | p.setDiscardOldest(value.toBool()); |
811 | changed |= QOpcUaMonitoringParameters::Parameter::DiscardOldest; |
812 | } |
813 | |
814 | if (item == QOpcUaMonitoringParameters::Parameter::Filter) { |
815 | changed |= QOpcUaMonitoringParameters::Parameter::Filter; |
816 | if (value.canConvert<QOpcUaMonitoringParameters::DataChangeFilter>()) |
817 | p.setFilter(value.value<QOpcUaMonitoringParameters::DataChangeFilter>()); |
818 | else if (value.canConvert<QOpcUaMonitoringParameters::EventFilter>()) |
819 | p.setFilter(value.value<QOpcUaMonitoringParameters::EventFilter>()); |
820 | if (res.results[0].filterResult.content.decoded.type == &UA_TYPES[UA_TYPES_EVENTFILTERRESULT]) |
821 | p.setFilterResult(convertEventFilterResult(obj: &res.results[0].filterResult)); |
822 | } |
823 | |
824 | emit m_backend->monitoringStatusChanged(handle: nodeHandle, attr, items: changed, param: p); |
825 | |
826 | monItem->parameters = p; |
827 | } |
828 | return true; |
829 | } |
830 | return false; |
831 | } |
832 | |
833 | QT_END_NAMESPACE |
834 | |