DeterministicESPAsyncWebServer v6.28.0
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
opcua_client.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file opcua_client.cpp
6 * @brief OPC UA Binary client - request builders + response parsers (implementation).
7 *
8 * Pure byte-buffer logic reusing the opcua.h codec; no transport, no heap, no stdlib.
9 */
10
12
13#if DETWS_ENABLE_OPCUA_CLIENT
14
15#include <string.h>
16
17void opcua_client_init(OpcUaClient *c)
18{
19 memset(c, 0, sizeof(*c));
20}
21
22// ---------------------------------------------------------------------------
23// Builder helpers
24// ---------------------------------------------------------------------------
25static size_t cw_patch(UaWriter *w)
26{
27 if (!w->ok)
28 return 0;
29 w->o[4] = (uint8_t)w->n;
30 w->o[5] = (uint8_t)(w->n >> 8);
31 w->o[6] = (uint8_t)(w->n >> 16);
32 w->o[7] = (uint8_t)(w->n >> 24);
33 return w->n;
34}
35
36// RequestHeader: the session AuthenticationToken (if available and requested) or a
37// null NodeId, then the fixed trailer. RequestHandle increments per request.
38static void cw_request_header(OpcUaClient *c, UaWriter *w, bool with_session_token)
39{
40 if (with_session_token && c->session_auth_id)
41 ua_w_nodeid_numeric(w, c->session_auth_ns, c->session_auth_id);
42 else
43 ua_w_nodeid_numeric(w, 0, 0);
44 ua_w_u64(w, 0); // Timestamp (unset)
45 ua_w_u32(w, ++c->request_handle); // RequestHandle
46 ua_w_u32(w, 0); // ReturnDiagnostics
47 ua_w_string(w, nullptr, -1); // AuditEntryId
48 ua_w_u32(w, 0); // TimeoutHint
49 ua_w_nodeid_numeric(w, 0, 0); // AdditionalHeader NodeId (null)
50 ua_w_u8(w, 0x00); // ... ExtensionObject "no body"
51}
52
53// MSG envelope prefix: header (size placeholder) + SymmetricSecurityHeader (TokenId)
54// + SequenceHeader (SequenceNumber, RequestId) + body TypeId.
55static void cw_msg(OpcUaClient *c, UaWriter *w, uint32_t type_id)
56{
57 ua_w_u8(w, 'M');
58 ua_w_u8(w, 'S');
59 ua_w_u8(w, 'G');
60 ua_w_u8(w, 'F');
61 ua_w_u32(w, 0); // size placeholder
62 ua_w_u32(w, c->channel_id); // SecureChannelId
63 ua_w_u32(w, c->token_id); // SymmetricSecurityHeader.TokenId
64 ua_w_u32(w, ++c->seq); // SequenceNumber
65 ua_w_u32(w, ++c->request_id); // RequestId
66 ua_w_nodeid_numeric(w, 0, type_id);
67}
68
69// ---------------------------------------------------------------------------
70// Request builders
71// ---------------------------------------------------------------------------
72size_t opcua_client_hello(const char *endpoint_url, uint8_t *out, size_t cap)
73{
74 UaWriter w = {out, cap, 0, true};
75 ua_w_u8(&w, 'H');
76 ua_w_u8(&w, 'E');
77 ua_w_u8(&w, 'L');
78 ua_w_u8(&w, 'F');
79 ua_w_u32(&w, 0); // size placeholder
80 ua_w_u32(&w, 0); // ProtocolVersion
81 ua_w_u32(&w, DETWS_OPCUA_BUF); // ReceiveBufferSize
82 ua_w_u32(&w, DETWS_OPCUA_BUF); // SendBufferSize
83 ua_w_u32(&w, 0); // MaxMessageSize (no limit)
84 ua_w_u32(&w, 0); // MaxChunkCount
85 ua_w_string(&w, endpoint_url, endpoint_url ? (int32_t)strnlen(endpoint_url, w.cap) : -1);
86 return cw_patch(&w);
87}
88
89size_t opcua_client_open(OpcUaClient *c, uint8_t *out, size_t cap)
90{
91 UaWriter w = {out, cap, 0, true};
92 ua_w_u8(&w, 'O');
93 ua_w_u8(&w, 'P');
94 ua_w_u8(&w, 'N');
95 ua_w_u8(&w, 'F');
96 ua_w_u32(&w, 0); // size placeholder
97 ua_w_u32(&w, 0); // SecureChannelId (0 = request a new channel)
98 ua_w_string(&w, OPCUA_POLICY_NONE_URI, (int32_t)(sizeof(OPCUA_POLICY_NONE_URI) - 1));
99 ua_w_string(&w, nullptr, -1); // SenderCertificate
100 ua_w_string(&w, nullptr, -1); // ReceiverCertificateThumbprint
101 ua_w_u32(&w, ++c->seq); // SequenceNumber
102 ua_w_u32(&w, ++c->request_id); // RequestId
103 ua_w_nodeid_numeric(&w, 0, OPCUA_ID_OPEN_REQ);
104 cw_request_header(c, &w, false);
105 ua_w_u32(&w, 0); // ClientProtocolVersion
106 ua_w_u32(&w, 0); // RequestType = Issue
107 ua_w_u32(&w, 1); // MessageSecurityMode = None
108 ua_w_string(&w, nullptr, -1); // ClientNonce
109 ua_w_u32(&w, 3600000); // RequestedLifetime (ms)
110 return cw_patch(&w);
111}
112
113size_t opcua_client_get_endpoints(OpcUaClient *c, const char *endpoint_url, uint8_t *out, size_t cap)
114{
115 UaWriter w = {out, cap, 0, true};
116 cw_msg(c, &w, OPCUA_ID_GET_ENDPOINTS_REQ);
117 cw_request_header(c, &w, false); // GetEndpoints needs no session
118 ua_w_string(&w, endpoint_url, endpoint_url ? (int32_t)strnlen(endpoint_url, w.cap) : -1); // EndpointUrl
119 ua_w_i32(&w, -1); // LocaleIds[] (null)
120 ua_w_i32(&w, -1); // ProfileUris[] (null)
121 return cw_patch(&w);
122}
123
124size_t opcua_client_create_session(OpcUaClient *c, const char *session_name, const char *endpoint_url, uint8_t *out,
125 size_t cap)
126{
127 UaWriter w = {out, cap, 0, true};
128 cw_msg(c, &w, OPCUA_ID_CREATE_SESSION_REQ);
129 cw_request_header(c, &w, false);
130 // ClientDescription (ApplicationDescription).
131 ua_w_string(&w, "urn:det:opcua:client", 20); // ApplicationUri
132 ua_w_string(&w, "urn:det:opcua", 13); // ProductUri
133 ua_w_localizedtext(&w, nullptr, "DetOpcUaClient"); // ApplicationName
134 ua_w_u32(&w, 1); // ApplicationType = Client
135 ua_w_string(&w, nullptr, -1); // GatewayServerUri
136 ua_w_string(&w, nullptr, -1); // DiscoveryProfileUri
137 ua_w_i32(&w, -1); // DiscoveryUrls[] (null)
138 ua_w_string(&w, nullptr, -1); // ServerUri
139 ua_w_string(&w, endpoint_url, endpoint_url ? (int32_t)strnlen(endpoint_url, w.cap) : -1); // EndpointUrl
140 ua_w_string(&w, session_name, session_name ? (int32_t)strnlen(session_name, w.cap) : -1); // SessionName
141 ua_w_string(&w, nullptr, -1); // ClientNonce (ByteString)
142 ua_w_string(&w, nullptr, -1); // ClientCertificate
143 ua_w_f64(&w, 1200000.0); // RequestedSessionTimeout
144 ua_w_u32(&w, 0); // MaxResponseMessageSize
145 return cw_patch(&w);
146}
147
148size_t opcua_client_activate_session(OpcUaClient *c, uint8_t *out, size_t cap)
149{
150 UaWriter w = {out, cap, 0, true};
151 cw_msg(c, &w, OPCUA_ID_ACTIVATE_SESSION_REQ);
152 cw_request_header(c, &w, true);
153 ua_w_string(&w, nullptr, -1); // ClientSignature.Algorithm
154 ua_w_string(&w, nullptr, -1); // ClientSignature.Signature
155 ua_w_i32(&w, -1); // ClientSoftwareCertificates[] (null)
156 ua_w_i32(&w, -1); // LocaleIds[] (null)
157 // UserIdentityToken: ExtensionObject(AnonymousIdentityToken i=321) with a String PolicyId body.
158 ua_w_nodeid_numeric(&w, 0, 321); // AnonymousIdentityToken_Encoding_DefaultBinary
159 ua_w_u8(&w, 0x01); // ExtensionObject body = ByteString
160 ua_w_i32(&w, 4 + 9); // body length = encoded String "anonymous" (4-byte len + 9 chars)
161 ua_w_string(&w, "anonymous", 9); // PolicyId
162 ua_w_string(&w, nullptr, -1); // UserTokenSignature.Algorithm
163 ua_w_string(&w, nullptr, -1); // UserTokenSignature.Signature
164 return cw_patch(&w);
165}
166
167size_t opcua_client_read(OpcUaClient *c, const OpcUaReadItem *items, uint32_t n, uint8_t *out, size_t cap)
168{
169 UaWriter w = {out, cap, 0, true};
170 cw_msg(c, &w, OPCUA_ID_READ_REQ);
171 cw_request_header(c, &w, true);
172 ua_w_f64(&w, 0.0); // MaxAge
173 ua_w_u32(&w, 0); // TimestampsToReturn (Source)
174 ua_w_i32(&w, (int32_t)n); // NodesToRead count
175 for (uint32_t i = 0; i < n; i++)
176 {
177 ua_w_nodeid_numeric(&w, items[i].ns, items[i].id);
178 ua_w_u32(&w, items[i].attribute);
179 ua_w_string(&w, nullptr, -1); // IndexRange
180 ua_w_u16(&w, 0); // DataEncoding QualifiedName.ns
181 ua_w_string(&w, nullptr, -1); // QualifiedName.name
182 }
183 return cw_patch(&w);
184}
185
186size_t opcua_client_browse(OpcUaClient *c, uint16_t ns, uint32_t id, uint8_t *out, size_t cap)
187{
188 UaWriter w = {out, cap, 0, true};
189 cw_msg(c, &w, OPCUA_ID_BROWSE_REQ);
190 cw_request_header(c, &w, true);
191 ua_w_nodeid_numeric(&w, 0, 0); // View.ViewId (null)
192 ua_w_u64(&w, 0); // View.Timestamp
193 ua_w_u32(&w, 0); // View.ViewVersion
194 ua_w_u32(&w, 0); // RequestedMaxReferencesPerNode
195 ua_w_i32(&w, 1); // NodesToBrowse count
196 ua_w_nodeid_numeric(&w, ns, id); // BrowseDescription.NodeId
197 ua_w_u32(&w, 0); // BrowseDirection (Forward)
198 ua_w_nodeid_numeric(&w, 0, 0); // ReferenceTypeId (null = all)
199 ua_w_bool(&w, true); // IncludeSubtypes
200 ua_w_u32(&w, 0); // NodeClassMask
201 ua_w_u32(&w, 0x3F); // ResultMask
202 return cw_patch(&w);
203}
204
205size_t opcua_client_write(OpcUaClient *c, const OpcUaWriteItem *items, uint32_t n, uint8_t *out, size_t cap)
206{
207 UaWriter w = {out, cap, 0, true};
208 cw_msg(c, &w, OPCUA_ID_WRITE_REQ);
209 cw_request_header(c, &w, true);
210 ua_w_i32(&w, (int32_t)n); // NodesToWrite count
211 for (uint32_t i = 0; i < n; i++)
212 {
213 ua_w_nodeid_numeric(&w, items[i].ns, items[i].id); // NodeId
214 ua_w_u32(&w, items[i].attribute); // AttributeId
215 ua_w_string(&w, nullptr, -1); // IndexRange
216 ua_w_datavalue(&w, &items[i].value, OPCUA_STATUS_GOOD); // Value (DataValue, value-only)
217 }
218 return cw_patch(&w);
219}
220
221size_t opcua_client_close_session(OpcUaClient *c, uint8_t *out, size_t cap)
222{
223 UaWriter w = {out, cap, 0, true};
224 cw_msg(c, &w, OPCUA_ID_CLOSE_SESSION_REQ);
225 cw_request_header(c, &w, true);
226 ua_w_bool(&w, true); // DeleteSubscriptions
227 return cw_patch(&w);
228}
229
230size_t opcua_client_close_channel(uint8_t *out, size_t cap)
231{
232 UaWriter w = {out, cap, 0, true};
233 ua_w_u8(&w, 'C');
234 ua_w_u8(&w, 'L');
235 ua_w_u8(&w, 'O');
236 ua_w_u8(&w, 'F');
237 ua_w_u32(&w, 0); // size placeholder (patched to 8)
238 return cw_patch(&w);
239}
240
241// ---------------------------------------------------------------------------
242// Parser helpers
243// ---------------------------------------------------------------------------
244static void cr_skip(UaReader *r, size_t n)
245{
246 if (r->err || r->off + n > r->len)
247 {
248 r->err = true;
249 return;
250 }
251 r->off += n;
252}
253
254static void cr_skip_string(UaReader *r)
255{
256 int32_t l = ua_r_i32(r);
257 if (l > 0)
258 cr_skip(r, (size_t)l);
259}
260
261static void cr_skip_string_array(UaReader *r)
262{
263 int32_t n = ua_r_i32(r);
264 for (int32_t i = 0; i < n; i++)
265 cr_skip_string(r);
266}
267
268// Consume a ResponseHeader (SecurityPolicy None encoding: empty diagnostics, no body
269// AdditionalHeader). Captures the ServiceResult.
270static bool cr_response_header(UaReader *r, uint32_t *service_result)
271{
272 (void)ua_r_u64(r); // Timestamp
273 (void)ua_r_u32(r); // RequestHandle
274 uint32_t svc = ua_r_u32(r);
275 if (service_result)
276 *service_result = svc;
277 (void)ua_r_u8(r); // ServiceDiagnostics (DiagnosticInfo, empty)
278 cr_skip_string_array(r); // StringTable
279 UaNodeId ah;
280 ua_r_nodeid(r, &ah); // AdditionalHeader NodeId
281 (void)ua_r_u8(r); // AdditionalHeader ExtensionObject (no body)
282 return !r->err;
283}
284
285// Open a MSG response: validate type + body TypeId, then consume the ResponseHeader.
286static bool cr_msg_open(const uint8_t *msg, size_t len, UaReader *r, uint32_t expect_type, uint32_t *service_result)
287{
288 UaMsgHeader h;
289 if (!opcua_parse_header(msg, len, &h) || memcmp(h.type, "MSG", 3) != 0 || h.size != len)
290 return false;
291 *r = UaReader{msg + 8, len - 8, 0, false};
292 (void)ua_r_u32(r); // SecureChannelId
293 (void)ua_r_u32(r); // TokenId
294 (void)ua_r_u32(r); // SequenceNumber
295 (void)ua_r_u32(r); // RequestId
296 UaNodeId tid;
297 if (!ua_r_nodeid(r, &tid))
298 return false;
299 if (!(tid.numeric && tid.id == expect_type))
300 return false;
301 return cr_response_header(r, service_result);
302}
303
304// ---------------------------------------------------------------------------
305// Response parsers
306// ---------------------------------------------------------------------------
307bool opcua_client_on_ack(const uint8_t *msg, size_t len, OpcUaAckInfo *out)
308{
309 UaMsgHeader h;
310 if (!opcua_parse_header(msg, len, &h) || memcmp(h.type, "ACK", 3) != 0)
311 return false;
312 if (h.size != len || len < 8 + 20)
313 return false;
314 UaReader r = {msg + 8, len - 8, 0, false};
315 (void)ua_r_u32(&r); // ProtocolVersion
316 out->recv_buf_size = ua_r_u32(&r);
317 out->send_buf_size = ua_r_u32(&r);
318 out->max_msg_size = ua_r_u32(&r);
319 out->max_chunk_count = ua_r_u32(&r);
320 return !r.err;
321}
322
323bool opcua_client_on_open(OpcUaClient *c, const uint8_t *msg, size_t len)
324{
325 UaMsgHeader h;
326 if (!opcua_parse_header(msg, len, &h) || memcmp(h.type, "OPN", 3) != 0 || h.size != len)
327 return false;
328 UaReader r = {msg + 8, len - 8, 0, false};
329 (void)ua_r_u32(&r); // SecureChannelId (asymmetric security header)
330 cr_skip_string(&r); // SecurityPolicyUri
331 cr_skip_string(&r); // SenderCertificate
332 cr_skip_string(&r); // ReceiverCertificateThumbprint
333 (void)ua_r_u32(&r); // SequenceNumber
334 (void)ua_r_u32(&r); // RequestId
335 UaNodeId tid;
336 if (!ua_r_nodeid(&r, &tid) || !(tid.numeric && tid.id == OPCUA_ID_OPEN_RESP))
337 return false;
338 uint32_t svc = 0;
339 if (!cr_response_header(&r, &svc))
340 return false;
341 (void)ua_r_u32(&r); // ServerProtocolVersion
342 c->channel_id = ua_r_u32(&r); // SecurityToken.ChannelId
343 c->token_id = ua_r_u32(&r); // SecurityToken.TokenId
344 (void)ua_r_u64(&r); // CreatedAt
345 (void)ua_r_u32(&r); // RevisedLifetime
346 return !r.err && svc == OPCUA_STATUS_GOOD;
347}
348
349int32_t opcua_client_on_get_endpoints(const uint8_t *msg, size_t len)
350{
351 UaReader r;
352 uint32_t svc = 0;
353 if (!cr_msg_open(msg, len, &r, OPCUA_ID_GET_ENDPOINTS_RESP, &svc) || svc != OPCUA_STATUS_GOOD)
354 return -1;
355 int32_t cnt = ua_r_i32(&r); // Endpoints[] count
356 return (cnt < 0 || r.err) ? -1 : cnt;
357}
358
359bool opcua_client_on_create_session(OpcUaClient *c, const uint8_t *msg, size_t len)
360{
361 UaReader r;
362 uint32_t svc = 0;
363 if (!cr_msg_open(msg, len, &r, OPCUA_ID_CREATE_SESSION_RESP, &svc))
364 return false;
365 UaNodeId sid;
366 ua_r_nodeid(&r, &sid); // SessionId
367 UaNodeId atok;
368 ua_r_nodeid(&r, &atok); // AuthenticationToken
369 c->session_auth_ns = atok.ns;
370 c->session_auth_id = atok.id;
371 c->session_auth_numeric = atok.numeric;
372 return !r.err && svc == OPCUA_STATUS_GOOD;
373}
374
375bool opcua_client_on_activate_session(const uint8_t *msg, size_t len)
376{
377 UaReader r;
378 uint32_t svc = 0;
379 if (!cr_msg_open(msg, len, &r, OPCUA_ID_ACTIVATE_SESSION_RESP, &svc))
380 return false;
381 return svc == OPCUA_STATUS_GOOD;
382}
383
384int32_t opcua_client_on_read(const uint8_t *msg, size_t len, OpcUaVariant *vals, uint32_t *statuses, uint32_t max)
385{
386 UaReader r;
387 uint32_t svc = 0;
388 if (!cr_msg_open(msg, len, &r, OPCUA_ID_READ_RESP, &svc) || svc != OPCUA_STATUS_GOOD)
389 return -1;
390 int32_t cnt = ua_r_i32(&r); // Results count
391 if (cnt < 0)
392 return -1;
393 uint32_t out_n = 0;
394 for (int32_t i = 0; i < cnt; i++)
395 {
396 uint8_t mask = ua_r_u8(&r);
397 OpcUaVariant v;
398 memset(&v, 0, sizeof(v));
399 uint32_t st = OPCUA_STATUS_GOOD;
400 if (mask & 0x01) // Value (Variant) present
401 {
402 OpcUaVariantType vt = (OpcUaVariantType)ua_r_u8(&r);
403 v.type = vt;
404 switch (vt)
405 {
406 case OpcUaVariantType::OPCUA_VAR_BOOL:
407 v.b = ua_r_bool(&r);
408 break;
409 case OpcUaVariantType::OPCUA_VAR_INT32:
410 v.i32 = ua_r_i32(&r);
411 break;
412 case OpcUaVariantType::OPCUA_VAR_UINT32:
413 v.u32 = ua_r_u32(&r);
414 break;
415 case OpcUaVariantType::OPCUA_VAR_FLOAT:
416 v.f32 = ua_r_f32(&r);
417 break;
418 case OpcUaVariantType::OPCUA_VAR_DOUBLE:
419 v.f64 = ua_r_f64(&r);
420 break;
421 case OpcUaVariantType::OPCUA_VAR_STRING: {
422 int32_t sl = ua_r_i32(&r);
423 v.str_len = sl;
424 if (sl > 0)
425 {
426 v.str = (const char *)(r.p + r.off); // points into msg
427 cr_skip(&r, (size_t)sl);
428 }
429 break;
430 }
431 default:
432 r.err = true; // unsupported Variant type
433 break;
434 }
435 }
436 if (mask & 0x02) // StatusCode present
437 st = ua_r_u32(&r);
438 if (out_n < max)
439 {
440 if (vals)
441 vals[out_n] = v;
442 if (statuses)
443 statuses[out_n] = st;
444 out_n++;
445 }
446 }
447 return r.err ? -1 : (int32_t)out_n;
448}
449
450int32_t opcua_client_on_write(const uint8_t *msg, size_t len, uint32_t *results, uint32_t max)
451{
452 UaReader r;
453 uint32_t svc = 0;
454 if (!cr_msg_open(msg, len, &r, OPCUA_ID_WRITE_RESP, &svc) || svc != OPCUA_STATUS_GOOD)
455 return -1;
456 int32_t cnt = ua_r_i32(&r); // Results count
457 if (cnt < 0)
458 return -1;
459 uint32_t out_n = 0;
460 for (int32_t i = 0; i < cnt; i++)
461 {
462 uint32_t st = ua_r_u32(&r);
463 if (out_n < max)
464 {
465 if (results)
466 results[out_n] = st;
467 out_n++;
468 }
469 }
470 return r.err ? -1 : (int32_t)out_n;
471}
472
473int32_t opcua_client_on_browse(const uint8_t *msg, size_t len, OpcUaClientRef *refs, uint32_t max)
474{
475 UaReader r;
476 uint32_t svc = 0;
477 if (!cr_msg_open(msg, len, &r, OPCUA_ID_BROWSE_RESP, &svc) || svc != OPCUA_STATUS_GOOD)
478 return -1;
479 int32_t nres = ua_r_i32(&r); // Results count
480 if (nres < 0)
481 return -1;
482 uint32_t out_n = 0;
483 for (int32_t ri = 0; ri < nres; ri++)
484 {
485 (void)ua_r_u32(&r); // BrowseResult.StatusCode
486 cr_skip_string(&r); // ContinuationPoint (ByteString)
487 int32_t nrefs = ua_r_i32(&r);
488 for (int32_t j = 0; j < nrefs; j++)
489 {
490 OpcUaClientRef ref;
491 memset(&ref, 0, sizeof(ref));
492 UaNodeId rt;
493 ua_r_nodeid(&r, &rt);
494 ref.ref_type_id = rt.id;
495 ref.is_forward = ua_r_bool(&r);
496 UaNodeId tgt;
497 ua_r_nodeid(&r, &tgt);
498 ref.target_ns = tgt.ns;
499 ref.target_id = tgt.id;
500 (void)ua_r_u16(&r); // BrowseName.NamespaceIndex
501 int32_t bnl = 0;
502 ua_r_string(&r, ref.browse_name, sizeof(ref.browse_name), &bnl);
503 uint8_t m = ua_r_u8(&r); // DisplayName LocalizedText mask
504 if (m & 0x01)
505 cr_skip_string(&r); // Locale
506 if (m & 0x02)
507 cr_skip_string(&r); // Text
508 ref.node_class = ua_r_u32(&r);
509 UaNodeId td;
510 ua_r_nodeid(&r, &td); // TypeDefinition
511 if (out_n < max)
512 {
513 if (refs)
514 refs[out_n] = ref;
515 out_n++;
516 }
517 }
518 }
519 return r.err ? -1 : (int32_t)out_n;
520}
521
522#endif // DETWS_ENABLE_OPCUA_CLIENT
OPC UA Binary client - request builders + response parsers (DETWS_ENABLE_OPCUA_CLIENT).