DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
opcua.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.cpp
6 * @brief OPC UA Binary server: handshake + SecureChannel + Session + Read/Write + Browse.
7 *
8 * Pure little-endian codec, handshake, OpenSecureChannel, CreateSession/
9 * ActivateSession, GetEndpoints, Read/Write (Variant/DataValue), Browse
10 * (ReferenceDescription), CloseSession and a ServiceFault fallback; the ESP32 section
11 * pumps the ConnProto::PROTO_OPCUA rx ring and answers HEL with ACK, OPN with an
12 * OpenSecureChannelResponse, the MSG service calls, and closes on CLO (SecurityPolicy
13 * None). No heap, no stdlib.
14 */
15
17
18#if DETWS_ENABLE_OPCUA
19
20#include <string.h>
21
22// ---------------------------------------------------------------------------
23// Built-in type codec
24// ---------------------------------------------------------------------------
25static void w_bytes(UaWriter *w, const void *src, size_t n)
26{
27 if (!w->ok || w->n + n > w->cap)
28 {
29 w->ok = false;
30 return;
31 }
32 memcpy(w->o + w->n, src, n); // NOSONAR - bound proven above; analyzer follows an infeasible path
33 w->n += n;
34}
35
36void ua_w_u8(UaWriter *w, uint8_t v)
37{
38 w_bytes(w, &v, 1);
39}
40void ua_w_u16(UaWriter *w, uint16_t v)
41{
42 uint8_t b[2] = {(uint8_t)v, (uint8_t)(v >> 8)};
43 w_bytes(w, b, 2);
44}
45void ua_w_u32(UaWriter *w, uint32_t v)
46{
47 uint8_t b[4] = {(uint8_t)v, (uint8_t)(v >> 8), (uint8_t)(v >> 16), (uint8_t)(v >> 24)};
48 w_bytes(w, b, 4);
49}
50void ua_w_u64(UaWriter *w, uint64_t v)
51{
52 uint8_t b[8];
53 for (int i = 0; i < 8; i++)
54 b[i] = (uint8_t)(v >> (8 * i));
55 w_bytes(w, b, 8);
56}
57void ua_w_i32(UaWriter *w, int32_t v)
58{
59 ua_w_u32(w, (uint32_t)v);
60}
61void ua_w_f32(UaWriter *w, float v)
62{
63 uint32_t u;
64 memcpy(&u, &v, 4);
65 ua_w_u32(w, u);
66}
67void ua_w_f64(UaWriter *w, double v)
68{
69 uint64_t u;
70 memcpy(&u, &v, 8);
71 ua_w_u64(w, u);
72}
73void ua_w_bool(UaWriter *w, bool v)
74{
75 ua_w_u8(w, v ? 1 : 0);
76}
77void ua_w_string(UaWriter *w, const char *s, int32_t len)
78{
79 ua_w_i32(w, len);
80 if (len > 0 && s)
81 w_bytes(w, s, (size_t)len);
82}
83
84static bool r_take(UaReader *r, void *dst, size_t n)
85{
86 if (r->err || r->off + n > r->len)
87 {
88 r->err = true;
89 return false;
90 }
91 memcpy(dst, r->p + r->off, n);
92 r->off += n;
93 return true;
94}
95
96uint8_t ua_r_u8(UaReader *r)
97{
98 uint8_t v = 0;
99 r_take(r, &v, 1);
100 return v;
101}
102uint16_t ua_r_u16(UaReader *r)
103{
104 uint8_t b[2] = {0, 0};
105 r_take(r, b, 2);
106 return (uint16_t)(b[0] | (b[1] << 8));
107}
108uint32_t ua_r_u32(UaReader *r)
109{
110 uint8_t b[4] = {0, 0, 0, 0};
111 r_take(r, b, 4);
112 return (uint32_t)b[0] | ((uint32_t)b[1] << 8) | ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24);
113}
114uint64_t ua_r_u64(UaReader *r)
115{
116 uint8_t b[8];
117 if (!r_take(r, b, 8))
118 return 0;
119 uint64_t v = 0;
120 for (int i = 0; i < 8; i++)
121 v |= (uint64_t)b[i] << (8 * i);
122 return v;
123}
124int32_t ua_r_i32(UaReader *r)
125{
126 return (int32_t)ua_r_u32(r);
127}
128float ua_r_f32(UaReader *r)
129{
130 uint32_t u = ua_r_u32(r);
131 float v;
132 memcpy(&v, &u, 4);
133 return v;
134}
135double ua_r_f64(UaReader *r)
136{
137 uint64_t u = ua_r_u64(r);
138 double v;
139 memcpy(&v, &u, 8);
140 return v;
141}
142bool ua_r_bool(UaReader *r)
143{
144 return ua_r_u8(r) != 0;
145}
146bool ua_r_string(UaReader *r, char *out, size_t cap, int32_t *out_len)
147{
148 int32_t len = ua_r_i32(r);
149 if (r->err)
150 return false;
151 if (out_len)
152 *out_len = len;
153 if (len < 0) // null string
154 {
155 if (cap)
156 out[0] = '\0';
157 return true;
158 }
159 if ((size_t)len + 1 > cap || r->off + (size_t)len > r->len)
160 {
161 r->err = true;
162 return false;
163 }
164 memcpy(out, r->p + r->off, (size_t)len);
165 out[len] = '\0';
166 r->off += (size_t)len;
167 return true;
168}
169
170static void r_skip(UaReader *r, size_t n)
171{
172 if (r->err || r->off + n > r->len)
173 {
174 r->err = true;
175 return;
176 }
177 r->off += n;
178}
179
180// ---------------------------------------------------------------------------
181// NodeId / ExtensionObject / DateTime
182// ---------------------------------------------------------------------------
183void ua_w_nodeid_numeric(UaWriter *w, uint16_t ns, uint32_t id)
184{
185 if (ns == 0 && id <= 0xFF) // TwoByte
186 {
187 ua_w_u8(w, 0x00);
188 ua_w_u8(w, (uint8_t)id);
189 }
190 else if (ns <= 0xFF && id <= 0xFFFF) // FourByte
191 {
192 ua_w_u8(w, 0x01);
193 ua_w_u8(w, (uint8_t)ns);
194 ua_w_u16(w, (uint16_t)id);
195 }
196 else // Numeric
197 {
198 ua_w_u8(w, 0x02);
199 ua_w_u16(w, ns);
200 ua_w_u32(w, id);
201 }
202}
203
204bool ua_r_nodeid(UaReader *r, UaNodeId *out)
205{
206 uint8_t enc = ua_r_u8(r);
207 uint8_t kind = enc & 0x0F; // strip the NamespaceUri (0x80) / ServerIndex (0x40) flags
208 out->ns = 0;
209 out->id = 0;
210 out->numeric = true;
211 switch (kind)
212 {
213 case 0x00: // TwoByte
214 out->id = ua_r_u8(r);
215 break;
216 case 0x01: // FourByte
217 out->ns = ua_r_u8(r);
218 out->id = ua_r_u16(r);
219 break;
220 case 0x02: // Numeric
221 out->ns = ua_r_u16(r);
222 out->id = ua_r_u32(r);
223 break;
224 case 0x03: // String
225 case 0x05: // ByteString
226 {
227 out->ns = ua_r_u16(r);
228 out->numeric = false;
229 int32_t l = ua_r_i32(r);
230 if (l > 0)
231 r_skip(r, (size_t)l);
232 break;
233 }
234 case 0x04: // Guid
235 out->ns = ua_r_u16(r);
236 out->numeric = false;
237 r_skip(r, 16);
238 break;
239 default:
240 r->err = true;
241 return false;
242 }
243 if (enc & 0x80) // NamespaceUri (String)
244 {
245 int32_t l = ua_r_i32(r);
246 if (l > 0)
247 r_skip(r, (size_t)l);
248 }
249 if (enc & 0x40) // ServerIndex (UInt32)
250 (void)ua_r_u32(r);
251 return !r->err;
252}
253
254// Skip an ExtensionObject: NodeId TypeId + encoding byte (+ ByteString/XML body).
255static bool r_ext_object_skip(UaReader *r)
256{
257 UaNodeId tid;
258 if (!ua_r_nodeid(r, &tid))
259 return false;
260 uint8_t body_enc = ua_r_u8(r);
261 if (body_enc == 0x00) // no body
262 return !r->err;
263 int32_t l = ua_r_i32(r); // ByteString (0x01) or XmlElement (0x02) body
264 if (l > 0)
265 r_skip(r, (size_t)l);
266 return !r->err;
267}
268
269// Read a RequestHeader (the prefix of every service request), capturing the
270// RequestHandle. The AuthenticationToken / Timestamp / diagnostics / audit id /
271// timeout / AdditionalHeader are consumed and discarded.
272static bool r_request_header(UaReader *r, uint32_t *request_handle)
273{
274 UaNodeId auth;
275 ua_r_nodeid(r, &auth); // AuthenticationToken
276 (void)ua_r_u64(r); // Timestamp (DateTime)
277 uint32_t rh = ua_r_u32(r); // RequestHandle
278 if (request_handle)
279 *request_handle = rh;
280 (void)ua_r_u32(r); // ReturnDiagnostics
281 int32_t aid = ua_r_i32(r); // AuditEntryId (String)
282 if (aid > 0)
283 r_skip(r, (size_t)aid);
284 (void)ua_r_u32(r); // TimeoutHint
285 return r_ext_object_skip(r); // AdditionalHeader (ExtensionObject)
286}
287
288// Parse a MSG-envelope preamble: security + sequence headers, body TypeId, and the RequestHeader.
289// On success r is positioned at the service body and m is filled; false on a malformed frame.
290static bool r_msg_preamble(const uint8_t *msg, size_t len, UaReader *r, OpcUaMsg *m)
291{
292 UaMsgHeader h;
293 if (!opcua_parse_header(msg, len, &h) || memcmp(h.type, "MSG", 3) != 0)
294 return false;
295 if (h.size != len)
296 return false;
297
298 UaReader rr = {msg + 8, len - 8, 0, false};
299 *r = rr;
300 m->secure_channel_id = ua_r_u32(r); // SecureChannelId
301 m->token_id = ua_r_u32(r);
302 m->sequence_number = ua_r_u32(r);
303 m->request_id = ua_r_u32(r);
304
305 UaNodeId tid;
306 if (!ua_r_nodeid(r, &tid)) // body TypeId
307 return false;
308 m->type_id = tid.numeric ? tid.id : 0;
309 return r_request_header(r, &m->request_handle);
310}
311
312int64_t opcua_filetime_from_unix(int64_t unix_seconds)
313{
314 if (unix_seconds <= 0)
315 return 0;
316 return (unix_seconds + 11644473600LL) * 10000000LL; // 1601->1970 offset, seconds -> 100 ns ticks
317}
318
319// ---------------------------------------------------------------------------
320// UACP framing + handshake
321// ---------------------------------------------------------------------------
322bool opcua_parse_header(const uint8_t *buf, size_t len, UaMsgHeader *h)
323{
324 if (!buf || len < 8 || !h)
325 return false;
326 h->type[0] = (char)buf[0];
327 h->type[1] = (char)buf[1];
328 h->type[2] = (char)buf[2];
329 h->chunk = (char)buf[3];
330 h->size = (uint32_t)buf[4] | ((uint32_t)buf[5] << 8) | ((uint32_t)buf[6] << 16) | ((uint32_t)buf[7] << 24);
331 return true;
332}
333
334bool opcua_parse_hello(const uint8_t *msg, size_t len, OpcUaHello *out)
335{
336 UaMsgHeader h;
337 if (!opcua_parse_header(msg, len, &h) || memcmp(h.type, "HEL", 3) != 0)
338 return false;
339 if (h.size != len || h.size < 8 + 20) // 8-byte header + at least the five sizes
340 return false;
341 UaReader r = {msg + 8, len - 8, 0, false};
342 out->protocol_version = ua_r_u32(&r);
343 out->recv_buf_size = ua_r_u32(&r);
344 out->send_buf_size = ua_r_u32(&r);
345 out->max_msg_size = ua_r_u32(&r);
346 out->max_chunk_count = ua_r_u32(&r);
347 return !r.err; // EndpointUrl (a String) follows; not needed for negotiation
348}
349
350static uint32_t neg(uint32_t client, uint32_t server)
351{
352 if (client == 0)
353 return server;
354 return client < server ? client : server;
355}
356
357size_t opcua_build_ack(const OpcUaHello *hello, uint8_t *out, size_t cap)
358{
359 if (!hello || !out)
360 return 0;
361 const uint32_t total = 8 + 20; // header + 5 x UInt32
362 UaWriter w = {out, cap, 0, true};
363 ua_w_u8(&w, 'A');
364 ua_w_u8(&w, 'C');
365 ua_w_u8(&w, 'K');
366 ua_w_u8(&w, 'F');
367 ua_w_u32(&w, total);
368 ua_w_u32(&w, 0); // ProtocolVersion
369 ua_w_u32(&w, neg(hello->send_buf_size, DETWS_OPCUA_BUF)); // our ReceiveBufferSize
370 ua_w_u32(&w, neg(hello->recv_buf_size, DETWS_OPCUA_BUF)); // our SendBufferSize
371 ua_w_u32(&w, neg(hello->max_msg_size, DETWS_OPCUA_BUF)); // MaxMessageSize
372 ua_w_u32(&w, 1); // MaxChunkCount (single-chunk)
373 return w.ok ? w.n : 0;
374}
375
376// ---------------------------------------------------------------------------
377// SecureChannel - OpenSecureChannel (OPN), SecurityPolicy None
378// ---------------------------------------------------------------------------
379bool opcua_parse_open(const uint8_t *msg, size_t len, OpcUaOpenChannel *out)
380{
381 UaMsgHeader h;
382 if (!opcua_parse_header(msg, len, &h) || memcmp(h.type, "OPN", 3) != 0)
383 return false;
384 if (h.size != len)
385 return false;
386
387 UaReader r = {msg + 8, len - 8, 0, false};
388
389 // Asymmetric security header (SecurityPolicy None -> no certs).
390 out->secure_channel_id = ua_r_u32(&r);
391 int32_t pol = ua_r_i32(&r); // SecurityPolicyUri (String)
392 if (pol > 0)
393 r_skip(&r, (size_t)pol);
394 int32_t sc = ua_r_i32(&r); // SenderCertificate (ByteString)
395 if (sc > 0)
396 r_skip(&r, (size_t)sc);
397 int32_t rt = ua_r_i32(&r); // ReceiverCertificateThumbprint (ByteString)
398 if (rt > 0)
399 r_skip(&r, (size_t)rt);
400
401 // Sequence header.
402 out->sequence_number = ua_r_u32(&r);
403 out->request_id = ua_r_u32(&r);
404
405 // Body: TypeId NodeId (must be OpenSecureChannelRequest).
406 UaNodeId tid;
407 if (!ua_r_nodeid(&r, &tid))
408 return false;
409 if (!(tid.numeric && tid.ns == 0 && tid.id == OPCUA_ID_OPEN_REQ))
410 return false;
411
412 // RequestHeader.
413 if (!r_request_header(&r, &out->request_handle))
414 return false;
415
416 // OpenSecureChannelRequest body.
417 out->client_protocol_version = ua_r_u32(&r);
418 out->security_token_request_type = ua_r_u32(&r);
419 out->message_security_mode = ua_r_u32(&r);
420 int32_t nonce = ua_r_i32(&r); // ClientNonce (ByteString)
421 if (nonce > 0)
422 r_skip(&r, (size_t)nonce);
423 out->requested_lifetime = ua_r_u32(&r);
424 return !r.err;
425}
426
427size_t opcua_build_open_response(const OpcUaOpenChannel *req, uint32_t channel_id, uint32_t token_id,
428 uint32_t seq_number, int64_t now_ft, uint32_t lifetime, uint8_t *out, size_t cap)
429{
430 if (!req || !out)
431 return 0;
432 UaWriter w = {out, cap, 0, true};
433
434 // Message header (size patched after).
435 ua_w_u8(&w, 'O');
436 ua_w_u8(&w, 'P');
437 ua_w_u8(&w, 'N');
438 ua_w_u8(&w, 'F');
439 ua_w_u32(&w, 0); // size placeholder
440
441 // Asymmetric security header (SecurityPolicy None: null sender cert + thumbprint).
442 ua_w_u32(&w, channel_id);
443 ua_w_string(&w, OPCUA_POLICY_NONE_URI, (int32_t)(sizeof(OPCUA_POLICY_NONE_URI) - 1));
444 ua_w_string(&w, nullptr, -1); // SenderCertificate
445 ua_w_string(&w, nullptr, -1); // ReceiverCertificateThumbprint
446
447 // Sequence header.
448 ua_w_u32(&w, seq_number);
449 ua_w_u32(&w, req->request_id); // RequestId echoed
450
451 // Body: TypeId = OpenSecureChannelResponse.
452 ua_w_nodeid_numeric(&w, 0, OPCUA_ID_OPEN_RESP);
453
454 // ResponseHeader.
455 ua_w_u64(&w, (uint64_t)now_ft); // Timestamp
456 ua_w_u32(&w, req->request_handle); // RequestHandle echoed
457 ua_w_u32(&w, 0); // ServiceResult = Good
458 ua_w_u8(&w, 0x00); // ServiceDiagnostics (DiagnosticInfo: no fields)
459 ua_w_i32(&w, -1); // StringTable (null array)
460 ua_w_nodeid_numeric(&w, 0, 0); // AdditionalHeader: null NodeId ...
461 ua_w_u8(&w, 0x00); // ... + ExtensionObject "no body"
462
463 // OpenSecureChannelResponse body.
464 ua_w_u32(&w, 0); // ServerProtocolVersion
465 ua_w_u32(&w, channel_id); // ChannelSecurityToken.ChannelId
466 ua_w_u32(&w, token_id); // .TokenId
467 ua_w_u64(&w, (uint64_t)now_ft); // .CreatedAt
468 ua_w_u32(&w, lifetime); // .RevisedLifetime
469 ua_w_string(&w, nullptr, -1); // ServerNonce (null for None)
470
471 if (!w.ok)
472 return 0;
473 out[4] = (uint8_t)w.n;
474 out[5] = (uint8_t)(w.n >> 8);
475 out[6] = (uint8_t)(w.n >> 16);
476 out[7] = (uint8_t)(w.n >> 24);
477 return w.n;
478}
479
480// ---------------------------------------------------------------------------
481// Session - CreateSession / ActivateSession (MSG service calls)
482// ---------------------------------------------------------------------------
483
484// Patch the 4-byte MessageSize field of a UACP message after the body is written.
485static size_t patch_size(UaWriter *w)
486{
487 if (!w->ok)
488 return 0;
489 w->o[4] = (uint8_t)w->n;
490 w->o[5] = (uint8_t)(w->n >> 8);
491 w->o[6] = (uint8_t)(w->n >> 16);
492 w->o[7] = (uint8_t)(w->n >> 24);
493 return w->n;
494}
495
496// Write a MSG envelope prefix: UACP header (size placeholder) + SecureChannelId +
497// SymmetricSecurityHeader (TokenId) + SequenceHeader (SequenceNumber, RequestId).
498static void w_msg_prefix(UaWriter *w, uint32_t channel_id, uint32_t token_id, uint32_t seq, uint32_t request_id)
499{
500 ua_w_u8(w, 'M');
501 ua_w_u8(w, 'S');
502 ua_w_u8(w, 'G');
503 ua_w_u8(w, 'F');
504 ua_w_u32(w, 0); // size placeholder
505 ua_w_u32(w, channel_id); // SecureChannelId
506 ua_w_u32(w, token_id); // SymmetricSecurityHeader.TokenId
507 ua_w_u32(w, seq); // SequenceHeader.SequenceNumber
508 ua_w_u32(w, request_id); // SequenceHeader.RequestId
509}
510
511// Write a ResponseHeader (the prefix of every service response).
512static void w_response_header(UaWriter *w, int64_t now_ft, uint32_t request_handle, uint32_t service_result)
513{
514 ua_w_u64(w, (uint64_t)now_ft); // Timestamp
515 ua_w_u32(w, request_handle); // RequestHandle echoed
516 ua_w_u32(w, service_result); // ServiceResult
517 ua_w_u8(w, 0x00); // ServiceDiagnostics (DiagnosticInfo: no fields)
518 ua_w_i32(w, -1); // StringTable (null array)
519 ua_w_nodeid_numeric(w, 0, 0); // AdditionalHeader: null NodeId ...
520 ua_w_u8(w, 0x00); // ... + ExtensionObject "no body"
521}
522
523bool opcua_parse_msg(const uint8_t *msg, size_t len, OpcUaMsg *out)
524{
525 UaMsgHeader h;
526 if (!opcua_parse_header(msg, len, &h) || memcmp(h.type, "MSG", 3) != 0)
527 return false;
528 if (h.size != len)
529 return false;
530
531 UaReader r = {msg + 8, len - 8, 0, false};
532 out->secure_channel_id = ua_r_u32(&r); // SecureChannelId
533 out->token_id = ua_r_u32(&r); // SymmetricSecurityHeader.TokenId
534 out->sequence_number = ua_r_u32(&r); // SequenceHeader.SequenceNumber
535 out->request_id = ua_r_u32(&r); // SequenceHeader.RequestId
536
537 UaNodeId tid;
538 if (!ua_r_nodeid(&r, &tid)) // body TypeId
539 return false;
540 out->type_id = tid.numeric ? tid.id : 0;
541
542 return r_request_header(&r, &out->request_handle);
543}
544
545// Transport profile URI for UA-TCP / UA-SecureConversation / UA Binary: an OPC UA
546// spec identifier string, never dereferenced as a URL.
547static const char OPCUA_TRANSPORT_URI[] =
548 "http://opcfoundation.org/UA-Profile/Transport/uatcp-uasc-uabinary"; // NOSONAR
549
550// The server-identity defaults (DETWS_DETWS_OPCUA_DEFAULT_ENDPOINT / _APP_URI / _APP_NAME) live in
551// ServerConfig.h under DETWS_ENABLE_OPCUA so a deployment can override them; used here for both
552// the struct default and the builder fallback so the two cannot drift apart.
553
554// All OPC UA agent state, owned by one instance (internal linkage): the advertised server
555// identity, the application Read/Write/Browse resolvers, and (ESP32 only) the per-channel
556// reassembly / response buffers and the SecureChannel + Session state (single client at a
557// time). Grouped so it is one named owner, unreachable from any other translation unit.
558struct OpcuaCtx
559{
560 OpcUaServerInfo server_info = {DETWS_OPCUA_DEFAULT_ENDPOINT, DETWS_OPCUA_DEFAULT_APP_URI,
561 DETWS_OPCUA_DEFAULT_APP_NAME};
562 OpcUaReadHandler read_handler = nullptr;
563 OpcUaWriteHandler write_handler = nullptr;
564 OpcUaBrowseHandler browse_handler = nullptr;
565#ifdef ARDUINO
566 uint8_t msg[DETWS_OPCUA_BUF]; // single-accessor reassembly buffer
567 uint8_t resp[2048]; // single-accessor response buffer (ACK / OPN / MSG response)
568 uint32_t channel_id = 0;
569 uint32_t token_id = 0;
570 uint32_t seq = 0;
571 uint32_t session_id = 0;
572 uint32_t auth_token = 0;
573#endif
574};
575static OpcuaCtx s_opcua;
576
577void opcua_set_endpoint_url(const char *url)
578{
579 s_opcua.server_info.endpoint_url = url;
580}
581
582void ua_w_endpoint_description(UaWriter *w, const OpcUaServerInfo *info)
583{
584 const char *url = (info && info->endpoint_url) ? info->endpoint_url : DETWS_OPCUA_DEFAULT_ENDPOINT;
585 const char *auri = (info && info->application_uri) ? info->application_uri : DETWS_OPCUA_DEFAULT_APP_URI;
586 const char *aname = (info && info->application_name) ? info->application_name : DETWS_OPCUA_DEFAULT_APP_NAME;
587
588 ua_w_string(w, url, (int32_t)strnlen(url, w->cap)); // EndpointUrl
589 // Server (ApplicationDescription).
590 ua_w_string(w, auri, (int32_t)strnlen(auri, w->cap)); // ApplicationUri
591 ua_w_string(w, "urn:det:opcua", 13); // ProductUri
592 ua_w_localizedtext(w, nullptr, aname); // ApplicationName
593 ua_w_u32(w, 0); // ApplicationType = Server
594 ua_w_string(w, nullptr, -1); // GatewayServerUri
595 ua_w_string(w, nullptr, -1); // DiscoveryProfileUri
596 ua_w_i32(w, -1); // DiscoveryUrls[] (null)
597 ua_w_string(w, nullptr, -1); // ServerCertificate (ByteString, null)
598 ua_w_u32(w, 1); // MessageSecurityMode = None
599 ua_w_string(w, OPCUA_POLICY_NONE_URI, (int32_t)(sizeof(OPCUA_POLICY_NONE_URI) - 1)); // SecurityPolicyUri
600 // UserIdentityTokens[] - one Anonymous policy.
601 ua_w_i32(w, 1);
602 ua_w_string(w, "anonymous", 9); // UserTokenPolicy.PolicyId
603 ua_w_u32(w, 0); // TokenType = Anonymous
604 ua_w_string(w, nullptr, -1); // IssuedTokenType
605 ua_w_string(w, nullptr, -1); // IssuerEndpointUrl
606 ua_w_string(w, nullptr, -1); // SecurityPolicyUri
607 ua_w_string(w, OPCUA_TRANSPORT_URI, (int32_t)(sizeof(OPCUA_TRANSPORT_URI) - 1)); // TransportProfileUri
608 ua_w_u8(w, 0); // SecurityLevel (Byte)
609}
610
611size_t opcua_build_create_session_response(const OpcUaMsg *req, uint32_t session_id, uint32_t auth_token,
612 double revised_timeout, const OpcUaServerInfo *info, uint32_t seq,
613 int64_t now_ft, uint8_t *out, size_t cap)
614{
615 if (!req || !out)
616 return 0;
617 UaWriter w = {out, cap, 0, true};
618 w_msg_prefix(&w, req->secure_channel_id, req->token_id, seq, req->request_id);
619 ua_w_nodeid_numeric(&w, 0, OPCUA_ID_CREATE_SESSION_RESP);
620 w_response_header(&w, now_ft, req->request_handle, 0);
621
622 ua_w_nodeid_numeric(&w, 1, session_id); // SessionId (server-assigned)
623 ua_w_nodeid_numeric(&w, 1, auth_token); // AuthenticationToken (server-assigned)
624 ua_w_f64(&w, revised_timeout); // RevisedSessionTimeout (ms)
625 ua_w_string(&w, nullptr, -1); // ServerNonce (none for SecurityPolicy None)
626 ua_w_string(&w, nullptr, -1); // ServerCertificate
627 ua_w_i32(&w, 1); // ServerEndpoints[] - advertise one None endpoint
628 ua_w_endpoint_description(&w, info);
629 ua_w_i32(&w, 0); // ServerSoftwareCertificates[] (empty)
630 ua_w_string(&w, nullptr, -1); // ServerSignature.Algorithm (null String)
631 ua_w_string(&w, nullptr, -1); // ServerSignature.Signature (null ByteString)
632 ua_w_u32(&w, 0); // MaxRequestMessageSize (0 = no limit)
633 return patch_size(&w);
634}
635
636size_t opcua_build_get_endpoints_response(const OpcUaMsg *req, const OpcUaServerInfo *info, uint32_t seq,
637 int64_t now_ft, uint8_t *out, size_t cap)
638{
639 if (!req || !out)
640 return 0;
641 UaWriter w = {out, cap, 0, true};
642 w_msg_prefix(&w, req->secure_channel_id, req->token_id, seq, req->request_id);
643 ua_w_nodeid_numeric(&w, 0, OPCUA_ID_GET_ENDPOINTS_RESP);
644 w_response_header(&w, now_ft, req->request_handle, 0);
645 ua_w_i32(&w, 1); // Endpoints[] - one SecurityPolicy None endpoint
646 ua_w_endpoint_description(&w, info);
647 return patch_size(&w);
648}
649
650size_t opcua_build_service_fault(const OpcUaMsg *req, uint32_t service_result, uint32_t seq, int64_t now_ft,
651 uint8_t *out, size_t cap)
652{
653 if (!req || !out)
654 return 0;
655 UaWriter w = {out, cap, 0, true};
656 w_msg_prefix(&w, req->secure_channel_id, req->token_id, seq, req->request_id);
657 ua_w_nodeid_numeric(&w, 0, OPCUA_ID_SERVICE_FAULT);
658 w_response_header(&w, now_ft, req->request_handle, service_result); // ServiceFault = ResponseHeader only
659 return patch_size(&w);
660}
661
662size_t opcua_build_activate_session_response(const OpcUaMsg *req, uint32_t seq, int64_t now_ft, uint8_t *out,
663 size_t cap)
664{
665 if (!req || !out)
666 return 0;
667 UaWriter w = {out, cap, 0, true};
668 w_msg_prefix(&w, req->secure_channel_id, req->token_id, seq, req->request_id);
669 ua_w_nodeid_numeric(&w, 0, OPCUA_ID_ACTIVATE_SESSION_RESP);
670 w_response_header(&w, now_ft, req->request_handle, 0);
671
672 ua_w_string(&w, nullptr, -1); // ServerNonce (none for SecurityPolicy None)
673 ua_w_i32(&w, 0); // Results[] (empty)
674 ua_w_i32(&w, 0); // DiagnosticInfos[] (empty)
675 return patch_size(&w);
676}
677
678// ---------------------------------------------------------------------------
679// Read service - Variant / DataValue encoding + ReadRequest/ReadResponse
680// ---------------------------------------------------------------------------
681void ua_w_variant(UaWriter *w, const OpcUaVariant *v)
682{
683 if (!v || v->type == OpcUaVariantType::OPCUA_VAR_NULL)
684 {
685 ua_w_u8(w, (uint8_t)OpcUaVariantType::OPCUA_VAR_NULL); // Null Variant (encoding byte 0)
686 return;
687 }
688 ua_w_u8(w, (uint8_t)v->type); // encoding byte = built-in type id (scalar; no array bits)
689 switch (v->type)
690 {
691 case OpcUaVariantType::OPCUA_VAR_BOOL:
692 ua_w_bool(w, v->b);
693 break;
694 case OpcUaVariantType::OPCUA_VAR_INT32:
695 ua_w_i32(w, v->i32);
696 break;
697 case OpcUaVariantType::OPCUA_VAR_UINT32:
698 ua_w_u32(w, v->u32);
699 break;
700 case OpcUaVariantType::OPCUA_VAR_FLOAT:
701 ua_w_f32(w, v->f32);
702 break;
703 case OpcUaVariantType::OPCUA_VAR_DOUBLE:
704 ua_w_f64(w, v->f64);
705 break;
706 case OpcUaVariantType::OPCUA_VAR_STRING:
707 ua_w_string(w, v->str, v->str_len);
708 break;
709 default:
710 w->ok = false; // unsupported type id: fail closed
711 break;
712 }
713}
714
715void ua_w_datavalue(UaWriter *w, const OpcUaVariant *v, uint32_t status)
716{
717 bool has_value = v && v->type != OpcUaVariantType::OPCUA_VAR_NULL;
718 uint8_t mask = 0;
719 if (has_value)
720 mask |= 0x01; // Value present
721 if (status != OPCUA_STATUS_GOOD)
722 mask |= 0x02; // StatusCode present
723 ua_w_u8(w, mask);
724 if (has_value)
725 ua_w_variant(w, v);
726 if (status != OPCUA_STATUS_GOOD)
727 ua_w_u32(w, status);
728}
729
730bool ua_r_variant(UaReader *r, OpcUaVariant *out)
731{
732 memset(out, 0, sizeof(*out));
733 uint8_t enc = ua_r_u8(r);
734 if (enc & 0x80) // array bit set: arrays are not supported by this scalar decoder
735 {
736 r->err = true;
737 return false;
738 }
739 out->type = (OpcUaVariantType)(enc & 0x3F); // built-in type id (mask off array dimension flags)
740 switch (out->type)
741 {
742 case OpcUaVariantType::OPCUA_VAR_NULL:
743 break;
744 case OpcUaVariantType::OPCUA_VAR_BOOL:
745 out->b = ua_r_bool(r);
746 break;
747 case OpcUaVariantType::OPCUA_VAR_INT32:
748 out->i32 = ua_r_i32(r);
749 break;
750 case OpcUaVariantType::OPCUA_VAR_UINT32:
751 out->u32 = ua_r_u32(r);
752 break;
753 case OpcUaVariantType::OPCUA_VAR_FLOAT:
754 out->f32 = ua_r_f32(r);
755 break;
756 case OpcUaVariantType::OPCUA_VAR_DOUBLE:
757 out->f64 = ua_r_f64(r);
758 break;
759 case OpcUaVariantType::OPCUA_VAR_STRING: {
760 int32_t sl = ua_r_i32(r);
761 out->str_len = sl;
762 if (sl > 0)
763 {
764 if (r->off + (size_t)sl > r->len)
765 {
766 r->err = true;
767 return false;
768 }
769 out->str = (const char *)(r->p + r->off); // points into the source buffer
770 r->off += (size_t)sl;
771 }
772 break;
773 }
774 default:
775 r->err = true; // unsupported built-in type
776 return false;
777 }
778 return !r->err;
779}
780
781bool ua_r_datavalue(UaReader *r, OpcUaVariant *out_value, uint32_t *out_status)
782{
783 memset(out_value, 0, sizeof(*out_value));
784 if (out_status)
785 *out_status = OPCUA_STATUS_GOOD;
786 uint8_t mask = ua_r_u8(r);
787 if (mask & 0x01) // Value (Variant)
788 {
789 if (!ua_r_variant(r, out_value))
790 return false;
791 }
792 if (mask & 0x02) // StatusCode
793 {
794 uint32_t st = ua_r_u32(r);
795 if (out_status)
796 *out_status = st;
797 }
798 if (mask & 0x04) // SourceTimestamp (DateTime)
799 (void)ua_r_u64(r);
800 if (mask & 0x10) // SourcePicoseconds (UInt16)
801 (void)ua_r_u16(r);
802 if (mask & 0x08) // ServerTimestamp (DateTime)
803 (void)ua_r_u64(r);
804 if (mask & 0x20) // ServerPicoseconds (UInt16)
805 (void)ua_r_u16(r);
806 return !r->err;
807}
808
809bool opcua_parse_read(const uint8_t *msg, size_t len, OpcUaReadRequest *out)
810{
811 UaReader r;
812 if (!r_msg_preamble(msg, len, &r, &out->msg))
813 return false;
814
815 // ReadRequest body.
816 (void)ua_r_f64(&r); // MaxAge
817 (void)ua_r_u32(&r); // TimestampsToReturn (enum)
818 int32_t cnt = ua_r_i32(&r); // NodesToRead array length
819 out->total = (cnt < 0) ? 0 : (uint32_t)cnt;
820 out->count = 0;
821 for (int32_t i = 0; i < cnt; i++)
822 {
823 UaNodeId nid;
824 if (!ua_r_nodeid(&r, &nid)) // ReadValueId.NodeId
825 return false;
826 uint32_t attr = ua_r_u32(&r); // AttributeId
827 int32_t ir = ua_r_i32(&r); // IndexRange (String)
828 if (ir > 0)
829 r_skip(&r, (size_t)ir);
830 (void)ua_r_u16(&r); // DataEncoding (QualifiedName) NamespaceIndex
831 int32_t qn = ua_r_i32(&r); // QualifiedName.Name (String)
832 if (qn > 0)
833 r_skip(&r, (size_t)qn);
834 if (out->count < DETWS_OPCUA_READ_MAX)
835 {
836 OpcUaReadItem *it = &out->items[out->count++];
837 it->ns = nid.ns;
838 it->id = nid.id;
839 it->numeric = nid.numeric;
840 it->attribute = attr;
841 }
842 }
843 return !r.err;
844}
845
846size_t opcua_build_read_response(const OpcUaReadRequest *req, const OpcUaVariant *values, const uint32_t *statuses,
847 uint32_t seq, int64_t now_ft, uint8_t *out, size_t cap)
848{
849 if (!req || !out)
850 return 0;
851 UaWriter w = {out, cap, 0, true};
852 w_msg_prefix(&w, req->msg.secure_channel_id, req->msg.token_id, seq, req->msg.request_id);
853 ua_w_nodeid_numeric(&w, 0, OPCUA_ID_READ_RESP);
854 w_response_header(&w, now_ft, req->msg.request_handle, 0);
855
856 ua_w_i32(&w, (int32_t)req->count); // Results[] (one DataValue per captured node)
857 for (uint32_t i = 0; i < req->count; i++)
858 ua_w_datavalue(&w, values ? &values[i] : nullptr, statuses ? statuses[i] : OPCUA_STATUS_GOOD);
859 ua_w_i32(&w, 0); // DiagnosticInfos[] (empty)
860 return patch_size(&w);
861}
862
863// Application Read resolver (set via opcua_set_read_handler), used by opcua_rx.
864void opcua_set_read_handler(OpcUaReadHandler fn)
865{
866 s_opcua.read_handler = fn;
867}
868
869// ---------------------------------------------------------------------------
870// Write service - WriteRequest/WriteResponse
871// ---------------------------------------------------------------------------
872bool opcua_parse_write(const uint8_t *msg, size_t len, OpcUaWriteRequest *out)
873{
874 UaReader r;
875 if (!r_msg_preamble(msg, len, &r, &out->msg))
876 return false;
877
878 int32_t cnt = ua_r_i32(&r); // NodesToWrite array length
879 out->total = (cnt < 0) ? 0 : (uint32_t)cnt;
880 out->count = 0;
881 for (int32_t i = 0; i < cnt; i++)
882 {
883 UaNodeId nid;
884 if (!ua_r_nodeid(&r, &nid)) // WriteValue.NodeId
885 return false;
886 uint32_t attr = ua_r_u32(&r); // AttributeId
887 int32_t ir = ua_r_i32(&r); // IndexRange (String)
888 if (ir > 0)
889 r_skip(&r, (size_t)ir);
890 OpcUaVariant val;
891 if (!ua_r_datavalue(&r, &val, nullptr)) // Value (DataValue)
892 return false;
893 if (out->count < DETWS_OPCUA_WRITE_MAX)
894 {
895 OpcUaWriteItem *it = &out->items[out->count++];
896 it->ns = nid.ns;
897 it->id = nid.id;
898 it->numeric = nid.numeric;
899 it->attribute = attr;
900 it->value = val;
901 }
902 }
903 return !r.err;
904}
905
906size_t opcua_build_write_response(const OpcUaWriteRequest *req, const uint32_t *results, uint32_t seq, int64_t now_ft,
907 uint8_t *out, size_t cap)
908{
909 if (!req || !out)
910 return 0;
911 UaWriter w = {out, cap, 0, true};
912 w_msg_prefix(&w, req->msg.secure_channel_id, req->msg.token_id, seq, req->msg.request_id);
913 ua_w_nodeid_numeric(&w, 0, OPCUA_ID_WRITE_RESP);
914 w_response_header(&w, now_ft, req->msg.request_handle, 0);
915
916 ua_w_i32(&w, (int32_t)req->count); // Results[] (one StatusCode per node)
917 for (uint32_t i = 0; i < req->count; i++)
918 ua_w_u32(&w, results ? results[i] : OPCUA_STATUS_GOOD);
919 ua_w_i32(&w, 0); // DiagnosticInfos[] (empty)
920 return patch_size(&w);
921}
922
923// Application Write resolver (set via opcua_set_write_handler), used by opcua_rx.
924void opcua_set_write_handler(OpcUaWriteHandler fn)
925{
926 s_opcua.write_handler = fn;
927}
928
929// ---------------------------------------------------------------------------
930// Browse service + CloseSession
931// ---------------------------------------------------------------------------
932void ua_w_qualifiedname(UaWriter *w, uint16_t ns, const char *name)
933{
934 ua_w_u16(w, ns);
935 ua_w_string(w, name, name ? (int32_t)strnlen(name, w->cap) : -1);
936}
937
938void ua_w_localizedtext(UaWriter *w, const char *locale, const char *text)
939{
940 uint8_t mask = 0;
941 if (locale)
942 mask |= 0x01; // GCOVR_EXCL_LINE Locale present; file-static, both callers pass a null locale -> dead
943 if (text)
944 mask |= 0x02; // Text present
945 ua_w_u8(w, mask);
946 if (locale)
947 ua_w_string(w, locale, (int32_t)strnlen(locale, w->cap)); // GCOVR_EXCL_LINE dead: no caller passes a locale
948 if (text)
949 ua_w_string(w, text, (int32_t)strnlen(text, w->cap));
950}
951
952void ua_w_reference(UaWriter *w, const OpcUaReference *ref)
953{
954 if (!ref)
955 {
956 w->ok = false; // GCOVR_EXCL_LINE dead: the sole caller (browse loop) always passes a valid &refs[j]
957 return; // GCOVR_EXCL_LINE
958 }
959 ua_w_nodeid_numeric(w, 0, ref->ref_type_id); // ReferenceTypeId
960 ua_w_bool(w, ref->is_forward); // IsForward
961 ua_w_nodeid_numeric(w, ref->target_ns, ref->target_id); // NodeId (ExpandedNodeId, numeric, no flags)
962 ua_w_qualifiedname(w, ref->browse_name_ns, ref->browse_name); // BrowseName
963 ua_w_localizedtext(w, nullptr, ref->display_name); // DisplayName
964 ua_w_u32(w, ref->node_class); // NodeClass
965 ua_w_nodeid_numeric(w, 0, ref->type_def_id); // TypeDefinition (ExpandedNodeId, numeric)
966}
967
968bool opcua_parse_browse(const uint8_t *msg, size_t len, OpcUaBrowseRequest *out)
969{
970 UaReader r;
971 if (!r_msg_preamble(msg, len, &r, &out->msg))
972 return false;
973
974 // BrowseRequest body: View (ViewDescription) + RequestedMaxReferencesPerNode + NodesToBrowse.
975 UaNodeId view;
976 ua_r_nodeid(&r, &view); // View.ViewId
977 (void)ua_r_u64(&r); // View.Timestamp
978 (void)ua_r_u32(&r); // View.ViewVersion
979 (void)ua_r_u32(&r); // RequestedMaxReferencesPerNode
980
981 int32_t cnt = ua_r_i32(&r); // NodesToBrowse array length
982 out->total = (cnt < 0) ? 0 : (uint32_t)cnt;
983 out->count = 0;
984 for (int32_t i = 0; i < cnt; i++)
985 {
986 UaNodeId nid;
987 if (!ua_r_nodeid(&r, &nid)) // BrowseDescription.NodeId
988 return false;
989 (void)ua_r_u32(&r); // BrowseDirection
990 UaNodeId rt;
991 ua_r_nodeid(&r, &rt); // ReferenceTypeId
992 (void)ua_r_bool(&r); // IncludeSubtypes
993 (void)ua_r_u32(&r); // NodeClassMask
994 (void)ua_r_u32(&r); // ResultMask
995 if (out->count < DETWS_OPCUA_BROWSE_MAX)
996 {
997 OpcUaBrowseItem *it = &out->items[out->count++];
998 it->ns = nid.ns;
999 it->id = nid.id;
1000 it->numeric = nid.numeric;
1001 }
1002 }
1003 return !r.err;
1004}
1005
1006size_t opcua_build_browse_response(const OpcUaBrowseRequest *req, OpcUaBrowseHandler handler, uint32_t seq,
1007 int64_t now_ft, uint8_t *out, size_t cap)
1008{
1009 if (!req || !out)
1010 return 0;
1011 UaWriter w = {out, cap, 0, true};
1012 w_msg_prefix(&w, req->msg.secure_channel_id, req->msg.token_id, seq, req->msg.request_id);
1013 ua_w_nodeid_numeric(&w, 0, OPCUA_ID_BROWSE_RESP);
1014 w_response_header(&w, now_ft, req->msg.request_handle, 0);
1015
1016 ua_w_i32(&w, (int32_t)req->count); // Results[] (one BrowseResult per browsed node)
1017 for (uint32_t i = 0; i < req->count; i++)
1018 {
1019 OpcUaReference refs[DETWS_OPCUA_REF_MAX];
1020 int32_t n = handler ? handler(req->items[i].ns, req->items[i].id, refs, DETWS_OPCUA_REF_MAX) : -1;
1021 uint32_t status = (n < 0) ? OPCUA_STATUS_BAD_NODE_ID_UNKNOWN : OPCUA_STATUS_GOOD;
1022 uint32_t nrefs = (n < 0) ? 0 : (uint32_t)n;
1023
1024 // BrowseResult.
1025 ua_w_u32(&w, status); // StatusCode
1026 ua_w_string(&w, nullptr, -1); // ContinuationPoint (ByteString, null)
1027 ua_w_i32(&w, (int32_t)nrefs); // References[]
1028 for (uint32_t j = 0; j < nrefs; j++)
1029 ua_w_reference(&w, &refs[j]);
1030 }
1031 ua_w_i32(&w, 0); // DiagnosticInfos[] (empty)
1032 return patch_size(&w);
1033}
1034
1035size_t opcua_build_close_session_response(const OpcUaMsg *req, uint32_t seq, int64_t now_ft, uint8_t *out, size_t cap)
1036{
1037 if (!req || !out)
1038 return 0;
1039 UaWriter w = {out, cap, 0, true};
1040 w_msg_prefix(&w, req->secure_channel_id, req->token_id, seq, req->request_id);
1041 ua_w_nodeid_numeric(&w, 0, OPCUA_ID_CLOSE_SESSION_RESP);
1042 w_response_header(&w, now_ft, req->request_handle, 0); // ResponseHeader only
1043 return patch_size(&w);
1044}
1045
1046// Application Browse resolver (set via opcua_set_browse_handler), used by opcua_rx.
1047void opcua_set_browse_handler(OpcUaBrowseHandler fn)
1048{
1049 s_opcua.browse_handler = fn;
1050}
1051
1052// ---------------------------------------------------------------------------
1053// ESP32 TCP server (ConnProto::PROTO_OPCUA)
1054// ---------------------------------------------------------------------------
1055#ifdef ARDUINO
1056
1059#include <time.h>
1060
1061namespace
1062{
1063// Thin adapters over the transport RX read API - the ring is owned by transport;
1064// this service never indexes rx_buffer or advances rx_tail itself.
1065size_t ring_avail(const TcpConn *c)
1066{
1067 return det_conn_available(c->id);
1068}
1069void ring_peek(const TcpConn *c, size_t off, uint8_t *dst, size_t n)
1070{
1071 det_conn_peek(c->id, off, dst, n);
1072}
1073void ring_consume(TcpConn *c, size_t n)
1074{
1075 det_conn_consume(c->id, n);
1076}
1077void raw_send(uint8_t slot, const void *data, size_t n)
1078{
1079 if (!det_conn_active(slot) || n == 0)
1080 return;
1081 det_conn_send(slot, data, (u16_t)n);
1082 det_conn_flush(slot);
1083}
1084void close_conn(uint8_t slot)
1085{
1086 det_conn_close(slot); // transport owns detach + slot reset + close
1087}
1088
1089} // namespace
1090
1091void opcua_rx(uint8_t slot)
1092{
1093 if (!det_conn_active(slot))
1094 return;
1095 TcpConn *c = &conn_pool[slot];
1096
1097 // Drain every complete UACP message currently in the rx ring (a client may
1098 // pipeline HEL then OPN; each arrives framed by an 8-byte header + MessageSize).
1099 for (;;)
1100 {
1101 if (ring_avail(c) < 8)
1102 return; // need the UACP header
1103
1104 uint8_t hdr[8];
1105 ring_peek(c, 0, hdr, 8);
1106 UaMsgHeader h;
1107 if (!opcua_parse_header(hdr, 8, &h) || h.size < 8 || h.size > sizeof(s_opcua.msg))
1108 {
1109 close_conn(slot);
1110 return;
1111 }
1112 if (ring_avail(c) < h.size)
1113 return; // wait for the full message
1114
1115 ring_peek(c, 0, s_opcua.msg, h.size);
1116 ring_consume(c, h.size);
1117
1118 if (memcmp(h.type, "HEL", 3) == 0)
1119 {
1120 OpcUaHello hello;
1121 size_t n;
1122 if (opcua_parse_hello(s_opcua.msg, h.size, &hello) &&
1123 (n = opcua_build_ack(&hello, s_opcua.resp, sizeof(s_opcua.resp))) > 0)
1124 raw_send(slot, s_opcua.resp, n);
1125 else
1126 {
1127 close_conn(slot);
1128 return;
1129 }
1130 }
1131 else if (memcmp(h.type, "OPN", 3) == 0)
1132 {
1133 OpcUaOpenChannel oc;
1134 if (!opcua_parse_open(s_opcua.msg, h.size, &oc))
1135 {
1136 close_conn(slot);
1137 return;
1138 }
1139 if (oc.secure_channel_id == 0) // fresh issue -> assign a channel id
1140 oc.secure_channel_id = ++s_opcua.channel_id;
1141 uint32_t token = ++s_opcua.token_id;
1142 uint32_t seq = ++s_opcua.seq;
1143 uint32_t lifetime = oc.requested_lifetime ? oc.requested_lifetime : 3600000u;
1144 int64_t now = opcua_filetime_from_unix((int64_t)time(nullptr));
1145 size_t n = opcua_build_open_response(&oc, oc.secure_channel_id, token, seq, now, lifetime, s_opcua.resp,
1146 sizeof(s_opcua.resp));
1147 if (n > 0)
1148 raw_send(slot, s_opcua.resp, n);
1149 else
1150 {
1151 close_conn(slot);
1152 return;
1153 }
1154 }
1155 else if (memcmp(h.type, "MSG", 3) == 0)
1156 {
1157 OpcUaMsg m;
1158 if (!opcua_parse_msg(s_opcua.msg, h.size, &m))
1159 {
1160 close_conn(slot);
1161 return;
1162 }
1163 int64_t now = opcua_filetime_from_unix((int64_t)time(nullptr));
1164 uint32_t seq = ++s_opcua.seq;
1165 size_t n = 0;
1166 if (m.type_id == OPCUA_ID_GET_ENDPOINTS_REQ)
1167 n = opcua_build_get_endpoints_response(&m, &s_opcua.server_info, seq, now, s_opcua.resp,
1168 sizeof(s_opcua.resp));
1169 else if (m.type_id == OPCUA_ID_CREATE_SESSION_REQ)
1170 n = opcua_build_create_session_response(&m, ++s_opcua.session_id, ++s_opcua.auth_token, 1200000.0,
1171 &s_opcua.server_info, seq, now, s_opcua.resp,
1172 sizeof(s_opcua.resp));
1173 else if (m.type_id == OPCUA_ID_ACTIVATE_SESSION_REQ)
1174 n = opcua_build_activate_session_response(&m, seq, now, s_opcua.resp, sizeof(s_opcua.resp));
1175 else if (m.type_id == OPCUA_ID_READ_REQ)
1176 {
1177 OpcUaReadRequest rr;
1178 if (!opcua_parse_read(s_opcua.msg, h.size, &rr))
1179 {
1180 close_conn(slot);
1181 return;
1182 }
1183 OpcUaVariant vals[DETWS_OPCUA_READ_MAX];
1184 uint32_t sts[DETWS_OPCUA_READ_MAX];
1185 for (uint32_t i = 0; i < rr.count; i++)
1186 {
1187 memset(&vals[i], 0, sizeof(vals[i]));
1188 bool ok = s_opcua.read_handler &&
1189 s_opcua.read_handler(rr.items[i].ns, rr.items[i].id, rr.items[i].attribute, &vals[i]);
1190 sts[i] = ok ? OPCUA_STATUS_GOOD : OPCUA_STATUS_BAD_NODE_ID_UNKNOWN;
1191 }
1192 n = opcua_build_read_response(&rr, vals, sts, seq, now, s_opcua.resp, sizeof(s_opcua.resp));
1193 }
1194 else if (m.type_id == OPCUA_ID_BROWSE_REQ)
1195 {
1196 OpcUaBrowseRequest br;
1197 if (!opcua_parse_browse(s_opcua.msg, h.size, &br))
1198 {
1199 close_conn(slot);
1200 return;
1201 }
1202 n = opcua_build_browse_response(&br, s_opcua.browse_handler, seq, now, s_opcua.resp,
1203 sizeof(s_opcua.resp));
1204 }
1205 else if (m.type_id == OPCUA_ID_WRITE_REQ)
1206 {
1207 OpcUaWriteRequest wr;
1208 if (!opcua_parse_write(s_opcua.msg, h.size, &wr))
1209 {
1210 close_conn(slot);
1211 return;
1212 }
1213 uint32_t res[DETWS_OPCUA_WRITE_MAX];
1214 for (uint32_t i = 0; i < wr.count; i++)
1215 res[i] = s_opcua.write_handler ? s_opcua.write_handler(wr.items[i].ns, wr.items[i].id,
1216 wr.items[i].attribute, &wr.items[i].value)
1217 : OPCUA_STATUS_BAD_NODE_ID_UNKNOWN;
1218 n = opcua_build_write_response(&wr, res, seq, now, s_opcua.resp, sizeof(s_opcua.resp));
1219 }
1220 else if (m.type_id == OPCUA_ID_CLOSE_SESSION_REQ)
1221 n = opcua_build_close_session_response(&m, seq, now, s_opcua.resp, sizeof(s_opcua.resp));
1222 else // unknown/unsupported service -> ServiceFault (so the client never hangs)
1223 n = opcua_build_service_fault(&m, OPCUA_STATUS_BAD_SERVICE_UNSUPPORTED, seq, now, s_opcua.resp,
1224 sizeof(s_opcua.resp));
1225 if (n > 0)
1226 raw_send(slot, s_opcua.resp, n);
1227 }
1228 else if (memcmp(h.type, "CLO", 3) == 0)
1229 {
1230 close_conn(slot);
1231 return;
1232 }
1233 }
1234}
1235
1236// The OPC UA ProtoHandler (Layer 5 dispatch seam) - only a data handler; the handshake reads from
1237// the rx ring, so there is no per-connection accept/close/poll state. Returned by accessor (no
1238// session dependency); proto_register_builtins() installs it.
1239static const ProtoHandler s_opcua_handler = {nullptr, opcua_rx, nullptr, nullptr};
1240const ProtoHandler *opcua_proto_handler(void)
1241{
1242 return &s_opcua_handler;
1243}
1244
1245#else // host build: the codec/handshake are tested directly; rx is a no-op stub
1246
1247void opcua_rx(uint8_t slot)
1248{
1249 (void)slot;
1250}
1251const ProtoHandler *opcua_proto_handler(void)
1252{
1253 return nullptr;
1254}
1255
1256#endif // ARDUINO
1257
1258#endif // DETWS_ENABLE_OPCUA
OPC UA Binary server: handshake + SecureChannel + Session + Read/Write + Browse (DETWS_ENABLE_OPCUA).
Layer 5 (Session) - per-protocol connection handler dispatch table.
Per-protocol connection event/poll callbacks (Layer 5 dispatch vtable).
A single TCP connection context.
Definition tcp.h:72
uint8_t id
Fixed slot index (0 … MAX_CONNS-1).
Definition tcp.h:73
bool det_conn_send(uint8_t slot, const void *data, u16_t len)
Send len bytes on connection slot (copies data; TLS-aware).
Definition tcp.cpp:362
void det_conn_flush(uint8_t slot)
Flush queued bytes / finish the send on slot (TLS-aware).
Definition tcp.cpp:413
void det_conn_close(uint8_t slot)
Close connection slot gracefully (tcp_close), aborting if the FIN cannot be queued....
Definition tcp.cpp:467
TcpConn conn_pool[CONN_POOL_SLOTS]
Static pool of connection contexts. Defined in tcp.cpp. Sized CONN_POOL_SLOTS: MAX_CONNS TCP slots pl...
Definition tcp.cpp:347
Layer 4 (Transport) - TCP connection pool, ring buffers, and lwIP integration.