DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
opcua.h
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.h
6 * @brief OPC UA Binary server: handshake + SecureChannel + Session + Read/Write + Browse (DETWS_ENABLE_OPCUA).
7 *
8 * OPC UA (IEC 62541) is large; this is built in increments. **Increment 1** is the
9 * foundation every OPC UA server needs:
10 * - the OPC UA Binary built-in-type codec (little-endian writer/reader for
11 * Boolean, integers, Float/Double, String, ByteString - bounds-checked, the
12 * basis for every later structure/service),
13 * - the UA-TCP connection protocol (UACP) message framing
14 * (`MessageType` + chunk byte + `MessageSize`), and
15 * - the **Hello / Acknowledge** handshake (OPC UA Part 6 §7.1.2): parse a
16 * client `HEL`, negotiate buffer sizes, emit an `ACK`.
17 *
18 * The codec + framing + handshake are pure and host-tested; opcua_rx() is the
19 * ConnProto::PROTO_OPCUA TCP data handler (ESP32) - `listen(4840, ConnProto::PROTO_OPCUA)` and a client
20 * gets through the handshake. Session and the Read service are later increments.
21 * SecurityPolicy is None (no encryption) for now.
22 *
23 * **Increment 2** adds the SecureChannel: NodeId / ExtensionObject / DateTime
24 * encoding on top of the increment-1 codec, then parsing an `OpenSecureChannel`
25 * (OPN) request and answering with an `OpenSecureChannelResponse` - the server
26 * assigns a SecureChannelId + security token (SecurityPolicy None, OPC UA Part 6
27 * §7.1.3 / Part 4 §5.5.2).
28 *
29 * **Increment 3** adds the Session: `MSG` (secure conversation) service calls
30 * dispatched by their body TypeId - `CreateSession` (the server assigns a SessionId
31 * and AuthenticationToken) and `ActivateSession` (OPC UA Part 4 §5.6).
32 *
33 * **Increment 4** adds the `Read` service (OPC UA Part 4 §5.10): scalar Variant +
34 * DataValue encoding and a registered resolver that maps a NodeId/attribute to a
35 * value - the first service that returns live data.
36 *
37 * **Increment 5** adds the `Browse` service (OPC UA Part 4 §5.8.2) via a registered
38 * resolver (ReferenceDescription / QualifiedName / LocalizedText encoding) and
39 * `CloseSession`; CloseSecureChannel arrives as a `CLO` message and closes the slot.
40 *
41 * Later increments add `GetEndpoints` + a `ServiceFault` fallback (so standard
42 * clients interoperate) and the `Write` service (DataValue/Variant decode + a
43 * registered write resolver). Verified end to end with python asyncua.
44 *
45 * No heap, no stdlib.
46 *
47 * @author Douglas Quigg (dstroy0)
48 * @date 2026
49 */
50
51#ifndef DETERMINISTICESPASYNCWEBSERVER_OPCUA_H
52#define DETERMINISTICESPASYNCWEBSERVER_OPCUA_H
53
54#include "ServerConfig.h"
55#include <stddef.h>
56#include <stdint.h>
57
58#if DETWS_ENABLE_OPCUA
59
60// ---------------------------------------------------------------------------
61// OPC UA Binary built-in type codec (little-endian; OPC UA Part 6 §5.2)
62// ---------------------------------------------------------------------------
63
64/** @brief Bounds-checked little-endian writer. */
65struct UaWriter
66{
67 uint8_t *o;
68 size_t cap;
69 size_t n;
70 bool ok;
71};
72void ua_w_u8(UaWriter *w, uint8_t v);
73void ua_w_u16(UaWriter *w, uint16_t v);
74void ua_w_u32(UaWriter *w, uint32_t v);
75void ua_w_u64(UaWriter *w, uint64_t v);
76void ua_w_i32(UaWriter *w, int32_t v);
77void ua_w_f32(UaWriter *w, float v);
78void ua_w_f64(UaWriter *w, double v);
79void ua_w_bool(UaWriter *w, bool v);
80/** @brief Encode a String/ByteString: int32 length (-1 = null) then the bytes. */
81void ua_w_string(UaWriter *w, const char *s, int32_t len);
82
83/** @brief Bounds-checked little-endian reader; @c err latches on underrun. */
84struct UaReader
85{
86 const uint8_t *p;
87 size_t len;
88 size_t off;
89 bool err;
90};
91uint8_t ua_r_u8(UaReader *r);
92uint16_t ua_r_u16(UaReader *r);
93uint32_t ua_r_u32(UaReader *r);
94uint64_t ua_r_u64(UaReader *r);
95int32_t ua_r_i32(UaReader *r);
96float ua_r_f32(UaReader *r);
97double ua_r_f64(UaReader *r);
98bool ua_r_bool(UaReader *r);
99/**
100 * @brief Decode a String/ByteString into @p out (NUL-terminated, bounded).
101 * @param out_len set to the decoded length (or -1 for a null string).
102 * @return false on underrun or if the value does not fit @p cap.
103 */
104bool ua_r_string(UaReader *r, char *out, size_t cap, int32_t *out_len);
105
106// ---------------------------------------------------------------------------
107// UA-TCP (UACP) message framing
108// ---------------------------------------------------------------------------
109
110/** @brief Parsed UACP message header (8 bytes). */
111struct UaMsgHeader
112{
113 char type[3]; ///< "HEL" / "ACK" / "ERR" / "OPN" / "MSG" / "CLO".
114 char chunk; ///< 'F' final, 'C' intermediate, 'A' abort.
115 uint32_t size; ///< total message size including this 8-byte header.
116};
117
118/** @brief Parse the 8-byte UACP header from @p buf (need >= 8 bytes). */
119bool opcua_parse_header(const uint8_t *buf, size_t len, UaMsgHeader *h);
120
121// ---------------------------------------------------------------------------
122// Hello / Acknowledge handshake (OPC UA Part 6 §7.1.2)
123// ---------------------------------------------------------------------------
124
125/** @brief Decoded Hello message body. */
126struct OpcUaHello
127{
128 uint32_t protocol_version;
129 uint32_t recv_buf_size;
130 uint32_t send_buf_size;
131 uint32_t max_msg_size;
132 uint32_t max_chunk_count;
133};
134
135/** @brief Parse a complete `HEL` message (header + body). @return true if valid. */
136bool opcua_parse_hello(const uint8_t *msg, size_t len, OpcUaHello *out);
137
138/**
139 * @brief Build the `ACK` reply to a parsed Hello, negotiating buffer sizes down
140 * to the server's DETWS_OPCUA_BUF limit.
141 * @return total ACK message bytes written to @p out, or 0 if it does not fit.
142 */
143size_t opcua_build_ack(const OpcUaHello *hello, uint8_t *out, size_t cap);
144
145// ---------------------------------------------------------------------------
146// NodeId / ExtensionObject / DateTime (OPC UA Part 6 §5.2.2)
147// ---------------------------------------------------------------------------
148
149/** @brief Numeric NodeId (the only kind the SecureChannel service needs). */
150struct UaNodeId
151{
152 uint16_t ns; ///< NamespaceIndex.
153 uint32_t id; ///< Numeric identifier.
154 bool numeric; ///< false for a String/Guid/ByteString id (value skipped on read).
155};
156
157/** @brief Encode a numeric NodeId, picking the smallest of the TwoByte/FourByte/Numeric forms. */
158void ua_w_nodeid_numeric(UaWriter *w, uint16_t ns, uint32_t id);
159
160/**
161 * @brief Decode a NodeId. Numeric forms fill @p out; String/Guid/ByteString ids
162 * are skipped (out->numeric=false). Latches @c err on an unknown form.
163 */
164bool ua_r_nodeid(UaReader *r, UaNodeId *out);
165
166/** @brief Convert a Unix epoch (seconds) to an OPC UA DateTime (100 ns ticks since 1601), 0 for <= 0. */
167int64_t opcua_filetime_from_unix(int64_t unix_seconds);
168
169/** @brief Numeric NodeIds (namespace 0) the SecureChannel service uses (binary encoding ids). */
170#define OPCUA_ID_OPEN_REQ 446 ///< OpenSecureChannelRequest_Encoding_DefaultBinary.
171#define OPCUA_ID_OPEN_RESP 449 ///< OpenSecureChannelResponse_Encoding_DefaultBinary.
172
173/** @brief SecurityPolicy "None" URI (no signing/encryption). */
174#define OPCUA_POLICY_NONE_URI "http://opcfoundation.org/UA/SecurityPolicy#None"
175
176// ---------------------------------------------------------------------------
177// SecureChannel - OpenSecureChannel (OPN), SecurityPolicy None
178// ---------------------------------------------------------------------------
179
180/** @brief Fields extracted from an OpenSecureChannelRequest we need to reply. */
181struct OpcUaOpenChannel
182{
183 uint32_t secure_channel_id; ///< 0 on a fresh issue; non-zero on renew.
184 uint32_t sequence_number; ///< Client SequenceHeader SequenceNumber.
185 uint32_t request_id; ///< Client SequenceHeader RequestId (echoed).
186 uint32_t request_handle; ///< RequestHeader RequestHandle (echoed).
187 uint32_t client_protocol_version; ///< ClientProtocolVersion.
188 uint32_t security_token_request_type; ///< 0 = Issue, 1 = Renew.
189 uint32_t message_security_mode; ///< 1 = None, 2 = Sign, 3 = SignAndEncrypt.
190 uint32_t requested_lifetime; ///< RequestedLifetime (ms).
191};
192
193/** @brief Parse a complete `OPN` message (SecurityPolicy None). @return true if valid. */
194bool opcua_parse_open(const uint8_t *msg, size_t len, OpcUaOpenChannel *out);
195
196/**
197 * @brief Build the `OPN` OpenSecureChannelResponse to a parsed request.
198 * @param req the parsed request (RequestId/RequestHandle are echoed).
199 * @param channel_id SecureChannelId the server assigns/keeps.
200 * @param token_id security TokenId the server issues.
201 * @param seq_number server SequenceNumber for this message.
202 * @param now_ft OPC UA DateTime for the response/token timestamps (0 = unset).
203 * @param lifetime RevisedLifetime (ms) granted to the token.
204 * @return total OPN message bytes written, or 0 if it does not fit @p cap.
205 */
206size_t opcua_build_open_response(const OpcUaOpenChannel *req, uint32_t channel_id, uint32_t token_id,
207 uint32_t seq_number, int64_t now_ft, uint32_t lifetime, uint8_t *out, size_t cap);
208
209// ---------------------------------------------------------------------------
210// Session - CreateSession / ActivateSession (MSG service calls, SecurityPolicy None)
211// ---------------------------------------------------------------------------
212
213/** @brief Numeric NodeIds (namespace 0) for the Session services (binary encoding ids). */
214#define OPCUA_ID_CREATE_SESSION_REQ 461 ///< CreateSessionRequest_Encoding_DefaultBinary.
215#define OPCUA_ID_CREATE_SESSION_RESP 464 ///< CreateSessionResponse_Encoding_DefaultBinary.
216#define OPCUA_ID_ACTIVATE_SESSION_REQ 467 ///< ActivateSessionRequest_Encoding_DefaultBinary.
217#define OPCUA_ID_ACTIVATE_SESSION_RESP 470 ///< ActivateSessionResponse_Encoding_DefaultBinary.
218
219/** @brief Common fields of a `MSG` (secure conversation) service request. */
220struct OpcUaMsg
221{
222 uint32_t secure_channel_id; ///< SecureChannelId (echoed in the response).
223 uint32_t token_id; ///< SymmetricSecurityHeader TokenId.
224 uint32_t sequence_number; ///< SequenceHeader SequenceNumber.
225 uint32_t request_id; ///< SequenceHeader RequestId (echoed in the response).
226 uint32_t type_id; ///< Body TypeId NodeId (numeric id; 0 if non-numeric).
227 uint32_t request_handle; ///< RequestHeader RequestHandle (echoed in the response).
228};
229
230/**
231 * @brief Parse a `MSG` envelope: security + sequence headers, body TypeId, and the
232 * leading RequestHeader (every service request starts with one).
233 * @return true if valid. @p out->type_id selects the service to dispatch.
234 */
235bool opcua_parse_msg(const uint8_t *msg, size_t len, OpcUaMsg *out);
236
237/** @brief Identity the server advertises in GetEndpoints / CreateSession endpoint descriptions. */
238struct OpcUaServerInfo
239{
240 const char *endpoint_url; ///< e.g. "opc.tcp://192.168.1.85:4840".
241 const char *application_uri; ///< server ApplicationUri.
242 const char *application_name; ///< server ApplicationName (display text).
243};
244
245/**
246 * @brief Build a `MSG` CreateSessionResponse (SecurityPolicy None): assign a SessionId
247 * and AuthenticationToken and advertise one None endpoint in ServerEndpoints so
248 * a client's endpoint validation matches GetEndpoints.
249 * @param req the parsed request (TokenId/RequestId/RequestHandle echoed).
250 * @param session_id numeric SessionId identifier the server assigns (namespace 1).
251 * @param auth_token numeric AuthenticationToken the server assigns (namespace 1).
252 * @param revised_timeout RevisedSessionTimeout (ms).
253 * @param info server identity for the advertised endpoint (may be null for defaults).
254 * @param seq server SequenceNumber for this message.
255 * @param now_ft OPC UA DateTime for the response timestamp (0 = unset).
256 * @return total MSG bytes written, or 0 if it does not fit @p cap.
257 */
258size_t opcua_build_create_session_response(const OpcUaMsg *req, uint32_t session_id, uint32_t auth_token,
259 double revised_timeout, const OpcUaServerInfo *info, uint32_t seq,
260 int64_t now_ft, uint8_t *out, size_t cap);
261
262/**
263 * @brief Build a `MSG` ActivateSessionResponse (SecurityPolicy None): ServiceResult Good,
264 * empty ServerNonce / Results / DiagnosticInfos.
265 * @return total MSG bytes written, or 0 if it does not fit @p cap.
266 */
267size_t opcua_build_activate_session_response(const OpcUaMsg *req, uint32_t seq, int64_t now_ft, uint8_t *out,
268 size_t cap);
269
270// ---------------------------------------------------------------------------
271// Read service (MSG, SecurityPolicy None) + Variant / DataValue encoding
272// ---------------------------------------------------------------------------
273
274/** @brief Numeric NodeIds (namespace 0) for the Read service (binary encoding ids). */
275#define OPCUA_ID_READ_REQ 631 ///< ReadRequest_Encoding_DefaultBinary.
276#define OPCUA_ID_READ_RESP 634 ///< ReadResponse_Encoding_DefaultBinary.
277
278/** @brief The OPC UA Attribute id a Read usually wants (the node's Value). */
279#define OPCUA_ATTR_VALUE 13
280
281/** @brief StatusCodes the Read service returns. */
282#define OPCUA_STATUS_GOOD 0x00000000u
283#define OPCUA_STATUS_BAD_NODE_ID_UNKNOWN 0x80340000u
284
285/** @brief OPC UA built-in type ids for the scalar Variants the Read service encodes. */
286enum class OpcUaVariantType : uint8_t
287{
288 OPCUA_VAR_NULL = 0,
289 OPCUA_VAR_BOOL = 1,
290 OPCUA_VAR_INT32 = 6,
291 OPCUA_VAR_UINT32 = 7,
292 OPCUA_VAR_FLOAT = 10,
293 OPCUA_VAR_DOUBLE = 11,
294 OPCUA_VAR_STRING = 12,
295};
296
297/** @brief A scalar OPC UA Variant value (the supported built-in types). */
298struct OpcUaVariant
299{
300 OpcUaVariantType type; ///< 0 = null Variant.
301 bool b; ///< OpcUaVariantType::OPCUA_VAR_BOOL.
302 int32_t i32; ///< OpcUaVariantType::OPCUA_VAR_INT32.
303 uint32_t u32; ///< OpcUaVariantType::OPCUA_VAR_UINT32.
304 float f32; ///< OpcUaVariantType::OPCUA_VAR_FLOAT.
305 double f64; ///< OpcUaVariantType::OPCUA_VAR_DOUBLE.
306 const char *str; ///< OpcUaVariantType::OPCUA_VAR_STRING (referenced, not copied).
307 int32_t str_len; ///< string length (-1 = null string).
308};
309
310/** @brief Encode a scalar Variant: encoding byte (built-in type id) then the value. */
311void ua_w_variant(UaWriter *w, const OpcUaVariant *v);
312
313/** @brief Encode a DataValue: a value/status mask byte then the Variant and/or StatusCode. */
314void ua_w_datavalue(UaWriter *w, const OpcUaVariant *v, uint32_t status);
315
316/**
317 * @brief Decode a scalar Variant. A decoded OpcUaVariantType::OPCUA_VAR_STRING points into the source
318 * buffer (keep it alive). Non-scalar/array Variants latch @c err.
319 */
320bool ua_r_variant(UaReader *r, OpcUaVariant *out);
321
322/**
323 * @brief Decode a DataValue: the mask byte, then (if present) the Variant value and
324 * StatusCode; SourceTimestamp/ServerTimestamp (and picoseconds) are skipped.
325 * @param out_value filled with the value (type 0 if no value field present).
326 * @param out_status set to the StatusCode (0 if not present).
327 */
328bool ua_r_datavalue(UaReader *r, OpcUaVariant *out_value, uint32_t *out_status);
329
330/** @brief One NodeId + attribute the client wants to read (a ReadValueId). */
331struct OpcUaReadItem
332{
333 uint16_t ns;
334 uint32_t id;
335 bool numeric;
336 uint32_t attribute;
337};
338
339/** @brief Parsed ReadRequest: the MSG envelope plus the (bounded) NodesToRead list. */
340struct OpcUaReadRequest
341{
342 OpcUaMsg msg; ///< envelope + RequestHeader (type_id = ReadRequest).
343 uint32_t total; ///< nodes requested (may exceed the captured count).
344 uint32_t count; ///< nodes captured (clamped to DETWS_OPCUA_READ_MAX).
345 OpcUaReadItem items[DETWS_OPCUA_READ_MAX];
346};
347
348/** @brief Parse a `MSG` ReadRequest. @return true if valid. */
349bool opcua_parse_read(const uint8_t *msg, size_t len, OpcUaReadRequest *out);
350
351/**
352 * @brief Build a `MSG` ReadResponse: one DataValue per captured NodesToRead entry.
353 * @param values per-node values (values[i] paired with req->items[i]); null type = no value.
354 * @param statuses per-node StatusCodes (0 = Good).
355 * @return total MSG bytes written, or 0 if it does not fit @p cap.
356 */
357size_t opcua_build_read_response(const OpcUaReadRequest *req, const OpcUaVariant *values, const uint32_t *statuses,
358 uint32_t seq, int64_t now_ft, uint8_t *out, size_t cap);
359
360/**
361 * @brief Application Read resolver: fill @p out for (ns, id, attribute). Return false
362 * for an unknown node/attribute (the server answers BadNodeIdUnknown).
363 */
364typedef bool (*OpcUaReadHandler)(uint16_t ns, uint32_t id, uint32_t attribute, OpcUaVariant *out);
365
366/** @brief Register the Read resolver the ConnProto::PROTO_OPCUA server calls for each ReadRequest node. */
367void opcua_set_read_handler(OpcUaReadHandler fn);
368
369// ---------------------------------------------------------------------------
370// Browse service + Close (MSG, SecurityPolicy None)
371// ---------------------------------------------------------------------------
372
373/** @brief Numeric NodeIds (namespace 0) for Browse / Close services (binary encoding ids). */
374#define OPCUA_ID_BROWSE_REQ 527 ///< BrowseRequest_Encoding_DefaultBinary.
375#define OPCUA_ID_BROWSE_RESP 530 ///< BrowseResponse_Encoding_DefaultBinary.
376#define OPCUA_ID_CLOSE_SESSION_REQ 473 ///< CloseSessionRequest_Encoding_DefaultBinary.
377#define OPCUA_ID_CLOSE_SESSION_RESP 476 ///< CloseSessionResponse_Encoding_DefaultBinary.
378
379/** @brief Common NodeClass / ReferenceType / TypeDefinition ids (namespace 0) for Browse results. */
380#define OPCUA_NODECLASS_OBJECT 1
381#define OPCUA_NODECLASS_VARIABLE 2
382#define OPCUA_REFTYPE_ORGANIZES 35
383#define OPCUA_REFTYPE_HAS_COMPONENT 47
384#define OPCUA_TYPEDEF_BASE_OBJECT 58
385#define OPCUA_TYPEDEF_BASE_DATA_VARIABLE 63
386
387/** @brief Encode a QualifiedName: NamespaceIndex (UInt16) + Name (String). */
388void ua_w_qualifiedname(UaWriter *w, uint16_t ns, const char *name);
389
390/** @brief Encode a LocalizedText: a present-fields mask then the optional Locale / Text Strings. */
391void ua_w_localizedtext(UaWriter *w, const char *locale, const char *text);
392
393/** @brief One reference (ReferenceDescription) returned by a Browse. Strings are referenced, not copied. */
394struct OpcUaReference
395{
396 uint32_t ref_type_id; ///< ReferenceType NodeId numeric id (e.g. OPCUA_REFTYPE_ORGANIZES).
397 bool is_forward; ///< IsForward.
398 uint16_t target_ns; ///< target NodeId namespace.
399 uint32_t target_id; ///< target NodeId numeric id.
400 uint16_t browse_name_ns; ///< BrowseName namespace.
401 const char *browse_name; ///< BrowseName.Name.
402 const char *display_name; ///< DisplayName text.
403 uint32_t node_class; ///< NodeClass (e.g. OPCUA_NODECLASS_VARIABLE).
404 uint32_t type_def_id; ///< TypeDefinition NodeId numeric id (e.g. OPCUA_TYPEDEF_BASE_DATA_VARIABLE).
405};
406
407/** @brief Encode a ReferenceDescription. */
408void ua_w_reference(UaWriter *w, const OpcUaReference *ref);
409
410/** @brief One NodeId the client wants to browse (a BrowseDescription). */
411struct OpcUaBrowseItem
412{
413 uint16_t ns;
414 uint32_t id;
415 bool numeric;
416};
417
418/** @brief Parsed BrowseRequest: the MSG envelope plus the (bounded) NodesToBrowse list. */
419struct OpcUaBrowseRequest
420{
421 OpcUaMsg msg;
422 uint32_t total;
423 uint32_t count;
424 OpcUaBrowseItem items[DETWS_OPCUA_BROWSE_MAX];
425};
426
427/** @brief Parse a `MSG` BrowseRequest. @return true if valid. */
428bool opcua_parse_browse(const uint8_t *msg, size_t len, OpcUaBrowseRequest *out);
429
430/**
431 * @brief Application Browse resolver: write up to @p max references for (ns, id) into
432 * @p out. @return the count written, or -1 for an unknown node (the server
433 * answers BadNodeIdUnknown for that BrowseResult).
434 */
435typedef int32_t (*OpcUaBrowseHandler)(uint16_t ns, uint32_t id, OpcUaReference *out, uint32_t max);
436
437/** @brief Register the Browse resolver the ConnProto::PROTO_OPCUA server calls for each browsed node. */
438void opcua_set_browse_handler(OpcUaBrowseHandler fn);
439
440/**
441 * @brief Build a `MSG` BrowseResponse: one BrowseResult per browsed node, each with the
442 * references the @p handler returns (up to DETWS_OPCUA_REF_MAX).
443 * @return total MSG bytes written, or 0 if it does not fit @p cap.
444 */
445size_t opcua_build_browse_response(const OpcUaBrowseRequest *req, OpcUaBrowseHandler handler, uint32_t seq,
446 int64_t now_ft, uint8_t *out, size_t cap);
447
448/** @brief Build a `MSG` CloseSessionResponse (ResponseHeader only, ServiceResult Good). */
449size_t opcua_build_close_session_response(const OpcUaMsg *req, uint32_t seq, int64_t now_ft, uint8_t *out, size_t cap);
450
451// ---------------------------------------------------------------------------
452// GetEndpoints + ServiceFault (third-party client interop)
453// ---------------------------------------------------------------------------
454
455/** @brief Numeric NodeIds (namespace 0) for GetEndpoints / ServiceFault (binary encoding ids). */
456#define OPCUA_ID_GET_ENDPOINTS_REQ 428 ///< GetEndpointsRequest_Encoding_DefaultBinary.
457#define OPCUA_ID_GET_ENDPOINTS_RESP 431 ///< GetEndpointsResponse_Encoding_DefaultBinary.
458#define OPCUA_ID_SERVICE_FAULT 397 ///< ServiceFault_Encoding_DefaultBinary.
459
460/** @brief StatusCode for an unsupported service (returned in a ServiceFault). */
461#define OPCUA_STATUS_BAD_SERVICE_UNSUPPORTED 0x800B0000u
462
463/** @brief Encode one EndpointDescription (SecurityMode/Policy None, one Anonymous user-token policy). */
464void ua_w_endpoint_description(UaWriter *w, const OpcUaServerInfo *info);
465
466/** @brief Build a `MSG` GetEndpointsResponse advertising a single SecurityPolicy None endpoint. */
467size_t opcua_build_get_endpoints_response(const OpcUaMsg *req, const OpcUaServerInfo *info, uint32_t seq,
468 int64_t now_ft, uint8_t *out, size_t cap);
469
470/**
471 * @brief Build a `MSG` ServiceFault (TypeId i=397) - a bare ResponseHeader carrying
472 * @p service_result, the reply for an unsupported/unknown service request.
473 */
474size_t opcua_build_service_fault(const OpcUaMsg *req, uint32_t service_result, uint32_t seq, int64_t now_ft,
475 uint8_t *out, size_t cap);
476
477/** @brief Set the endpoint URL the ConnProto::PROTO_OPCUA server advertises (GetEndpoints / CreateSession). */
478void opcua_set_endpoint_url(const char *url);
479
480// ---------------------------------------------------------------------------
481// Write service (MSG, SecurityPolicy None)
482// ---------------------------------------------------------------------------
483
484/** @brief Numeric NodeIds (namespace 0) for the Write service (binary encoding ids). */
485#define OPCUA_ID_WRITE_REQ 673 ///< WriteRequest_Encoding_DefaultBinary.
486#define OPCUA_ID_WRITE_RESP 676 ///< WriteResponse_Encoding_DefaultBinary.
487
488/** @brief StatusCode for a node/attribute that cannot be written. */
489#define OPCUA_STATUS_BAD_NOT_WRITABLE 0x803B0000u
490
491/** @brief One value the client wants to write (a WriteValue). */
492struct OpcUaWriteItem
493{
494 uint16_t ns;
495 uint32_t id;
496 bool numeric;
497 uint32_t attribute;
498 OpcUaVariant value; ///< the DataValue's Variant (string values point into the source buffer).
499};
500
501/** @brief Parsed WriteRequest: the MSG envelope plus the (bounded) NodesToWrite list. */
502struct OpcUaWriteRequest
503{
504 OpcUaMsg msg;
505 uint32_t total;
506 uint32_t count;
507 OpcUaWriteItem items[DETWS_OPCUA_WRITE_MAX];
508};
509
510/** @brief Parse a `MSG` WriteRequest. @return true if valid. */
511bool opcua_parse_write(const uint8_t *msg, size_t len, OpcUaWriteRequest *out);
512
513/**
514 * @brief Build a `MSG` WriteResponse: one StatusCode per NodesToWrite entry.
515 * @param results per-node StatusCodes (0 = Good); may be null (all Good).
516 * @return total MSG bytes written, or 0 if it does not fit @p cap.
517 */
518size_t opcua_build_write_response(const OpcUaWriteRequest *req, const uint32_t *results, uint32_t seq, int64_t now_ft,
519 uint8_t *out, size_t cap);
520
521/**
522 * @brief Application Write resolver: apply @p value to (ns, id, attribute) and return a
523 * StatusCode (0 = Good). Return a Bad code (e.g. OPCUA_STATUS_BAD_NODE_ID_UNKNOWN
524 * or OPCUA_STATUS_BAD_NOT_WRITABLE) to reject the write.
525 */
526typedef uint32_t (*OpcUaWriteHandler)(uint16_t ns, uint32_t id, uint32_t attribute, const OpcUaVariant *value);
527
528/** @brief Register the Write resolver the ConnProto::PROTO_OPCUA server calls for each WriteRequest node. */
529void opcua_set_write_handler(OpcUaWriteHandler fn);
530
531// ---------------------------------------------------------------------------
532// ESP32 TCP server (ConnProto::PROTO_OPCUA data handler)
533// ---------------------------------------------------------------------------
534
535/**
536 * @brief ConnProto::PROTO_OPCUA data handler: handshake, SecureChannel, Session, Read.
537 *
538 * Dispatched by the session layer for connections accepted on an OPC UA listener
539 * (`listen(4840, ConnProto::PROTO_OPCUA)`). Drains framed messages from the slot's rx ring:
540 * `HEL` -> negotiated `ACK`, `OPN` -> OpenSecureChannelResponse (SecurityPolicy
541 * None), `MSG` GetEndpoints/CreateSession/ActivateSession/Read/Write/Browse/
542 * CloseSession -> their responses (Read/Write/Browse call the registered resolvers;
543 * an unknown service draws a ServiceFault), `CLO` (CloseSecureChannel) -> close.
544 */
545void opcua_rx(uint8_t slot);
546
547/** @brief The OPC UA ProtoHandler (accessor; nullptr on host builds; installed by the builtins list). */
548struct ProtoHandler;
549const struct ProtoHandler *opcua_proto_handler(void);
550
551#endif // DETWS_ENABLE_OPCUA
552#endif // DETERMINISTICESPASYNCWEBSERVER_OPCUA_H
User-facing configuration for DeterministicESPAsyncWebServer.
Per-protocol connection event/poll callbacks (Layer 5 dispatch vtable).