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