ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
opcua.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - 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 (PROTOCORE_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; protocore_opcua_rx() is the
19 * ProtoConn::PROTO_OPCUA TCP data handler (ESP32) - `listen(4840, ProtoConn::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 PROTOCORE_OPCUA_H
52#define PROTOCORE_OPCUA_H
53
54#include "protocore_config.h" // the entry point: protocore_types.h for the widths
55
56#if PROTOCORE_ENABLE_OPCUA
57
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. */
65typedef struct
66{
67 uint8_t *o;
68 size_t cap;
69 size_t n;
70 proto_bool ok;
71} UaWriter;
72void protocore_ua_w_u8(UaWriter *w, uint8_t v);
73void protocore_ua_w_u16(UaWriter *w, uint16_t v);
74void protocore_ua_w_u32(UaWriter *w, uint32_t v);
75void protocore_ua_w_u64(UaWriter *w, uint64_t v);
76void protocore_ua_w_i32(UaWriter *w, int32_t v);
77void protocore_ua_w_f32(UaWriter *w, float v);
78void protocore_ua_w_f64(UaWriter *w, double v);
79void protocore_ua_w_bool(UaWriter *w, proto_bool v);
80/** @brief Encode a String/ByteString: int32 length (-1 = null) then the bytes. */
81void protocore_ua_w_string(UaWriter *w, const char *s, int32_t len);
82
83/** @brief Bounds-checked little-endian reader; @c err latches on underrun. */
84typedef struct
85{
86 const uint8_t *p;
87 size_t len;
88 size_t off;
89 proto_bool err;
90} UaReader;
91uint8_t protocore_ua_r_u8(UaReader *r);
92uint16_t protocore_ua_r_u16(UaReader *r);
93uint32_t protocore_ua_r_u32(UaReader *r);
94uint64_t protocore_ua_r_u64(UaReader *r);
95int32_t protocore_ua_r_i32(UaReader *r);
96float protocore_ua_r_f32(UaReader *r);
97double protocore_ua_r_f64(UaReader *r);
98proto_bool protocore_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 */
104proto_bool protocore_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). */
111typedef struct
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} UaMsgHeader;
117
118/** @brief Parse the 8-byte UACP header from @p buf (need >= 8 bytes). */
119proto_bool protocore_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. */
126typedef struct
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} OpcUaHello;
134
135/** @brief Parse a complete `HEL` message (header + body). @return true if valid. */
136proto_bool protocore_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 PROTOCORE_OPCUA_BUF limit.
141 * @return total ACK message bytes written to @p out, or 0 if it does not fit.
142 */
143size_t protocore_opcua_build_ack(const OpcUaHello *hello, uint8_t *out, size_t cap);
144
145// Common transport-level status codes for a UACP ERR message (OPC UA Part 6 §7.1.2.4).
146#define PROTOCORE_OPCUA_BAD_TCP_MESSAGE_TYPE_INVALID 0x807E0000u
147#define PROTOCORE_OPCUA_BAD_TCP_MESSAGE_TOO_LARGE 0x80800000u
148#define PROTOCORE_OPCUA_BAD_TCP_NOT_ENOUGH_RESOURCES 0x80810000u
149#define PROTOCORE_OPCUA_BAD_TCP_INTERNAL_ERROR 0x80820000u
150
151/**
152 * @brief Build a transport-level `ERR` message (the reply a server sends before closing when the handshake
153 * fails): `ERR F <size> <error:UInt32> <reason:String>`. @p error_code is an OPC UA StatusCode
154 * (PROTOCORE_OPCUA_BAD_TCP_*); @p reason is a short UTF-8 diagnostic (may be null for a null String).
155 * @return total ERR message bytes written to @p out, or 0 on a null buffer / overflow.
156 */
157size_t protocore_opcua_build_error(uint32_t error_code, const char *reason, uint8_t *out, size_t cap);
158
159// ---------------------------------------------------------------------------
160// NodeId / ExtensionObject / DateTime (OPC UA Part 6 §5.2.2)
161// ---------------------------------------------------------------------------
162
163/** @brief Numeric NodeId (the only kind the SecureChannel service needs). */
164typedef struct
165{
166 uint16_t ns; ///< NamespaceIndex.
167 uint32_t id; ///< Numeric identifier.
168 proto_bool numeric; ///< false for a String/Guid/ByteString id (value skipped on read).
169} UaNodeId;
170
171/** @brief Encode a numeric NodeId, picking the smallest of the TwoByte/FourByte/Numeric forms. */
172void protocore_ua_w_nodeid_numeric(UaWriter *w, uint16_t ns, uint32_t id);
173
174/**
175 * @brief Decode a NodeId. Numeric forms fill @p out; String/Guid/ByteString ids
176 * are skipped (out->numeric=false). Latches @c err on an unknown form.
177 */
178proto_bool protocore_ua_r_nodeid(UaReader *r, UaNodeId *out);
179
180/** @brief Convert a Unix epoch (seconds) to an OPC UA DateTime (100 ns ticks since 1601), 0 for <= 0. */
181int64_t protocore_opcua_filetime_from_unix(int64_t unix_seconds);
182
183/** @brief Numeric NodeIds (namespace 0) the SecureChannel service uses (binary encoding ids). */
184#define OPCUA_ID_OPEN_REQ 446 ///< OpenSecureChannelRequest_Encoding_DefaultBinary.
185#define OPCUA_ID_OPEN_RESP 449 ///< OpenSecureChannelResponse_Encoding_DefaultBinary.
186
187/** @brief SecurityPolicy "None" URI (no signing/encryption). */
188#define OPCUA_POLICY_NONE_URI "http://opcfoundation.org/UA/SecurityPolicy#None"
189
190// ---------------------------------------------------------------------------
191// SecureChannel - OpenSecureChannel (OPN), SecurityPolicy None
192// ---------------------------------------------------------------------------
193
194/** @brief Fields extracted from an OpenSecureChannelRequest we need to reply. */
195typedef struct
196{
197 uint32_t secure_channel_id; ///< 0 on a fresh issue; non-zero on renew.
198 uint32_t sequence_number; ///< Client SequenceHeader SequenceNumber.
199 uint32_t request_id; ///< Client SequenceHeader RequestId (echoed).
200 uint32_t request_handle; ///< RequestHeader RequestHandle (echoed).
201 uint32_t client_protocol_version; ///< ClientProtocolVersion.
202 uint32_t security_token_request_type; ///< 0 = Issue, 1 = Renew.
203 uint32_t message_security_mode; ///< 1 = None, 2 = Sign, 3 = SignAndEncrypt.
204 uint32_t requested_lifetime; ///< RequestedLifetime (ms).
205} OpcUaOpenChannel;
206
207/** @brief Parse a complete `OPN` message (SecurityPolicy None). @return true if valid. */
208proto_bool protocore_opcua_parse_open(const uint8_t *msg, size_t len, OpcUaOpenChannel *out);
209
210/**
211 * @brief Build the `OPN` OpenSecureChannelResponse to a parsed request.
212 * @param req the parsed request (RequestId/RequestHandle are echoed).
213 * @param channel_id SecureChannelId the server assigns/keeps.
214 * @param token_id security TokenId the server issues.
215 * @param seq_number server SequenceNumber for this message.
216 * @param now_ft OPC UA DateTime for the response/token timestamps (0 = unset).
217 * @param lifetime RevisedLifetime (ms) granted to the token.
218 * @return total OPN message bytes written, or 0 if it does not fit @p cap.
219 */
220size_t protocore_opcua_build_open_response(const OpcUaOpenChannel *req, uint32_t channel_id, uint32_t token_id,
221 uint32_t seq_number, int64_t now_ft, uint32_t lifetime, uint8_t *out,
222 size_t cap);
223
224// ---------------------------------------------------------------------------
225// Session - CreateSession / ActivateSession (MSG service calls, SecurityPolicy None)
226// ---------------------------------------------------------------------------
227
228/** @brief Numeric NodeIds (namespace 0) for the Session services (binary encoding ids). */
229#define OPCUA_ID_CREATE_SESSION_REQ 461 ///< CreateSessionRequest_Encoding_DefaultBinary.
230#define OPCUA_ID_CREATE_SESSION_RESP 464 ///< CreateSessionResponse_Encoding_DefaultBinary.
231#define OPCUA_ID_ACTIVATE_SESSION_REQ 467 ///< ActivateSessionRequest_Encoding_DefaultBinary.
232#define OPCUA_ID_ACTIVATE_SESSION_RESP 470 ///< ActivateSessionResponse_Encoding_DefaultBinary.
233
234/** @brief Common fields of a `MSG` (secure conversation) service request. */
235typedef struct
236{
237 uint32_t secure_channel_id; ///< SecureChannelId (echoed in the response).
238 uint32_t token_id; ///< SymmetricSecurityHeader TokenId.
239 uint32_t sequence_number; ///< SequenceHeader SequenceNumber.
240 uint32_t request_id; ///< SequenceHeader RequestId (echoed in the response).
241 uint32_t type_id; ///< Body TypeId NodeId (numeric id; 0 if non-numeric).
242 uint32_t request_handle; ///< RequestHeader RequestHandle (echoed in the response).
243} OpcUaMsg;
244
245/**
246 * @brief Parse a `MSG` envelope: security + sequence headers, body TypeId, and the
247 * leading RequestHeader (every service request starts with one).
248 * @return true if valid. @p out->type_id selects the service to dispatch.
249 */
250proto_bool protocore_opcua_parse_msg(const uint8_t *msg, size_t len, OpcUaMsg *out);
251
252/** @brief Identity the server advertises in GetEndpoints / CreateSession endpoint descriptions. */
253typedef struct
254{
255 const char *endpoint_url; ///< e.g. "opc.tcp://192.168.1.85:4840".
256 const char *application_uri; ///< server ApplicationUri.
257 const char *application_name; ///< server ApplicationName (display text).
258} OpcUaServerInfo;
259
260/**
261 * @brief Build a `MSG` CreateSessionResponse (SecurityPolicy None): assign a SessionId
262 * and AuthenticationToken and advertise one None endpoint in ServerEndpoints so
263 * a client's endpoint validation matches GetEndpoints.
264 * @param req the parsed request (TokenId/RequestId/RequestHandle echoed).
265 * @param session_id numeric SessionId identifier the server assigns (namespace 1).
266 * @param auth_token numeric AuthenticationToken the server assigns (namespace 1).
267 * @param revised_timeout RevisedSessionTimeout (ms).
268 * @param info server identity for the advertised endpoint (may be null for defaults).
269 * @param seq server SequenceNumber for this message.
270 * @param now_ft OPC UA DateTime for the response timestamp (0 = unset).
271 * @return total MSG bytes written, or 0 if it does not fit @p cap.
272 */
273size_t protocore_opcua_build_create_session_response(const OpcUaMsg *req, uint32_t session_id, uint32_t auth_token,
274 double revised_timeout, const OpcUaServerInfo *info, uint32_t seq,
275 int64_t now_ft, uint8_t *out, size_t cap);
276
277/**
278 * @brief Build a `MSG` ActivateSessionResponse (SecurityPolicy None): ServiceResult Good,
279 * empty ServerNonce / Results / DiagnosticInfos.
280 * @return total MSG bytes written, or 0 if it does not fit @p cap.
281 */
282size_t protocore_opcua_build_activate_session_response(const OpcUaMsg *req, uint32_t seq, int64_t now_ft, uint8_t *out,
283 size_t cap);
284
285// ---------------------------------------------------------------------------
286// Read service (MSG, SecurityPolicy None) + Variant / DataValue encoding
287// ---------------------------------------------------------------------------
288
289/** @brief Numeric NodeIds (namespace 0) for the Read service (binary encoding ids). */
290#define OPCUA_ID_READ_REQ 631 ///< ReadRequest_Encoding_DefaultBinary.
291#define OPCUA_ID_READ_RESP 634 ///< ReadResponse_Encoding_DefaultBinary.
292
293/** @brief The OPC UA Attribute id a Read usually wants (the node's Value). */
294#define OPCUA_ATTR_VALUE 13
295
296/** @brief StatusCodes the Read service returns. */
297#define OPCUA_STATUS_GOOD 0x00000000u
298#define OPCUA_STATUS_BAD_NODE_ID_UNKNOWN 0x80340000u
299
300/** @brief OPC UA built-in type ids for the scalar Variants the Read service encodes. */
301typedef enum PROTO_ENUM_PACKED
302{
303 OPCUA_VAR_NULL = 0,
304 OPCUA_VAR_BOOL = 1,
305 OPCUA_VAR_INT32 = 6,
306 OPCUA_VAR_UINT32 = 7,
307 OPCUA_VAR_INT64 = 8,
308 OPCUA_VAR_UINT64 = 9,
309 OPCUA_VAR_FLOAT = 10,
310 OPCUA_VAR_DOUBLE = 11,
311 OPCUA_VAR_STRING = 12,
312} OpcUaVariantType;
313
314/** @brief A scalar OPC UA Variant value (the supported built-in types). */
315typedef struct
316{
317 OpcUaVariantType type; ///< 0 = null Variant.
318 proto_bool b; ///< OPCUA_VAR_BOOL.
319 int32_t i32; ///< OPCUA_VAR_INT32.
320 uint32_t u32; ///< OPCUA_VAR_UINT32.
321 int64_t i64; ///< OPCUA_VAR_INT64.
322 uint64_t u64; ///< OPCUA_VAR_UINT64.
323 float f32; ///< OPCUA_VAR_FLOAT.
324 double f64; ///< OPCUA_VAR_DOUBLE.
325 const char *str; ///< OPCUA_VAR_STRING (referenced, not copied).
326 int32_t str_len; ///< string length (-1 = null string).
327} OpcUaVariant;
328
329/** @brief Encode a scalar Variant: encoding byte (built-in type id) then the value. */
330void protocore_ua_w_variant(UaWriter *w, const OpcUaVariant *v);
331
332/** @brief Encode a DataValue: a value/status mask byte then the Variant and/or StatusCode. */
333void protocore_ua_w_datavalue(UaWriter *w, const OpcUaVariant *v, uint32_t status);
334
335/**
336 * @brief Decode a scalar Variant. A decoded OPCUA_VAR_STRING points into the source
337 * buffer (keep it alive). Non-scalar/array Variants latch @c err.
338 */
339proto_bool protocore_ua_r_variant(UaReader *r, OpcUaVariant *out);
340
341/**
342 * @brief Decode a DataValue: the mask byte, then (if present) the Variant value and
343 * StatusCode; SourceTimestamp/ServerTimestamp (and picoseconds) are skipped.
344 * @param out_value filled with the value (type 0 if no value field present).
345 * @param out_status set to the StatusCode (0 if not present).
346 */
347proto_bool protocore_ua_r_datavalue(UaReader *r, OpcUaVariant *out_value, uint32_t *out_status);
348
349/** @brief One NodeId + attribute the client wants to read (a ReadValueId). */
350typedef struct
351{
352 uint16_t ns;
353 uint32_t id;
354 proto_bool numeric;
355 uint32_t attribute;
356} OpcUaReadItem;
357
358/** @brief Parsed ReadRequest: the MSG envelope plus the (bounded) NodesToRead list. */
359typedef struct
360{
361 OpcUaMsg msg; ///< envelope + RequestHeader (type_id = ReadRequest).
362 uint32_t total; ///< nodes requested (may exceed the captured count).
363 uint32_t count; ///< nodes captured (clamped to PROTOCORE_OPCUA_READ_MAX).
364 OpcUaReadItem items[PROTOCORE_OPCUA_READ_MAX];
365} OpcUaReadRequest;
366
367/** @brief Parse a `MSG` ReadRequest. @return true if valid. */
368proto_bool protocore_opcua_parse_read(const uint8_t *msg, size_t len, OpcUaReadRequest *out);
369
370/**
371 * @brief Build a `MSG` ReadResponse: one DataValue per captured NodesToRead entry.
372 * @param values per-node values (values[i] paired with req->items[i]); null type = no value.
373 * @param statuses per-node StatusCodes (0 = Good).
374 * @return total MSG bytes written, or 0 if it does not fit @p cap.
375 */
376size_t protocore_opcua_build_read_response(const OpcUaReadRequest *req, const OpcUaVariant *values,
377 const uint32_t *statuses, uint32_t seq, int64_t now_ft, uint8_t *out,
378 size_t cap);
379
380/**
381 * @brief Application Read resolver: fill @p out for (ns, id, attribute). Return false
382 * for an unknown node/attribute (the server answers BadNodeIdUnknown).
383 */
384typedef proto_bool (*OpcUaReadHandler)(uint16_t ns, uint32_t id, uint32_t attribute, OpcUaVariant *out);
385
386/** @brief Register the Read resolver the ProtoConn::PROTO_OPCUA server calls for each ReadRequest node. */
387void protocore_opcua_set_read_handler(OpcUaReadHandler fn);
388
389/** @brief The Read resolver registered above, or null while none is. */
390OpcUaReadHandler protocore_opcua_read_handler(void);
391
392// ---------------------------------------------------------------------------
393// Browse service + Close (MSG, SecurityPolicy None)
394// ---------------------------------------------------------------------------
395
396/** @brief Numeric NodeIds (namespace 0) for Browse / Close services (binary encoding ids). */
397#define OPCUA_ID_BROWSE_REQ 527 ///< BrowseRequest_Encoding_DefaultBinary.
398#define OPCUA_ID_BROWSE_RESP 530 ///< BrowseResponse_Encoding_DefaultBinary.
399#define OPCUA_ID_CLOSE_SESSION_REQ 473 ///< CloseSessionRequest_Encoding_DefaultBinary.
400#define OPCUA_ID_CLOSE_SESSION_RESP 476 ///< CloseSessionResponse_Encoding_DefaultBinary.
401
402/** @brief Common NodeClass / ReferenceType / TypeDefinition ids (namespace 0) for Browse results. */
403#define OPCUA_NODECLASS_OBJECT 1
404#define OPCUA_NODECLASS_VARIABLE 2
405#define OPCUA_REFTYPE_ORGANIZES 35
406#define OPCUA_REFTYPE_HAS_PROPERTY 46
407#define OPCUA_REFTYPE_HAS_COMPONENT 47
408/** @brief The reference an add-in hangs off its host by (Part 3): how DI and Machinery attach Identification. */
409#define OPCUA_REFTYPE_HAS_ADDIN 17604
410#define OPCUA_TYPEDEF_BASE_OBJECT 58
411/** @brief The type of a plain container of other nodes, as every published model types its folders. */
412#define OPCUA_TYPEDEF_FOLDER 61
413#define OPCUA_TYPEDEF_BASE_DATA_VARIABLE 63
414#define OPCUA_TYPEDEF_PROPERTY 68
415
416// ---------------------------------------------------------------------------
417// The NamespaceArray
418// ---------------------------------------------------------------------------
419//
420// Part 3 sec 8.2.2: a NodeId's NamespaceIndex is an index into the server's NamespaceArray, and
421// index 0 is always the OPC UA core namespace. The index is the SERVER's, not a model's, so a model
422// states the URI it publishes and the server answers with the index that URI got. Two models loaded
423// together therefore cannot both claim index 1, which is what a per-model compile-time constant did.
424
425/** @brief How many namespaces beyond the core one this server can hold. */
426#ifndef PROTOCORE_OPCUA_NAMESPACES
427#define PROTOCORE_OPCUA_NAMESPACES 4
428#endif
429
430/** @brief Index 0 of every server's NamespaceArray (Part 3 sec 8.2.2). */
431#define OPCUA_CORE_NS_URI "http://opcfoundation.org/UA/"
432
433/** @brief The longest namespace URI a compare walks. */
434#ifndef PROTOCORE_OPCUA_NS_URI_MAX
435#define PROTOCORE_OPCUA_NS_URI_MAX 128
436#endif
437
438/** @brief The namespace URI a register names. */
439typedef struct
440{
441 const char *uri; ///< the model's namespace URI, referenced and not copied
442} OpcUaNamespaceArgs;
443
444/**
445 * @brief Register @p uri and report the index it holds, adding it if it is not there yet.
446 *
447 * Reports 0 when the table is full: index 0 is the core namespace, which no model owns, so a caller
448 * that lands there knows it did not get one.
449 */
450uint16_t protocore_opcua_namespace_index(const char *uri);
451
452/** @brief The URI at @p index, or NULL past the end of the array. */
453const char *protocore_opcua_namespace_uri(uint16_t index);
454
455/** @brief How many namespaces the array holds, the core one included. */
456uint16_t protocore_opcua_namespace_count(void);
457
458/** @brief Encode a QualifiedName: NamespaceIndex (UInt16) + Name (String). */
459void protocore_ua_w_qualifiedname(UaWriter *w, uint16_t ns, const char *name);
460
461/** @brief Encode a LocalizedText: a present-fields mask then the optional Locale / Text Strings. */
462void protocore_ua_w_localizedtext(UaWriter *w, const char *locale, const char *text);
463
464/** @brief One reference (ReferenceDescription) returned by a Browse. Strings are referenced, not copied. */
465typedef struct
466{
467 uint32_t ref_type_id; ///< ReferenceType NodeId numeric id (e.g. OPCUA_REFTYPE_ORGANIZES).
468 proto_bool is_forward; ///< IsForward.
469 uint16_t target_ns; ///< target NodeId namespace.
470 uint32_t target_id; ///< target NodeId numeric id.
471 uint16_t browse_name_ns; ///< BrowseName namespace.
472 const char *browse_name; ///< BrowseName.Name.
473 const char *display_name; ///< DisplayName text.
474 uint32_t node_class; ///< NodeClass (e.g. OPCUA_NODECLASS_VARIABLE).
475 uint16_t type_def_ns; ///< TypeDefinition namespace index; 0 for the core types below.
476 uint32_t type_def_id; ///< TypeDefinition NodeId numeric id (e.g. OPCUA_TYPEDEF_BASE_DATA_VARIABLE).
477} OpcUaReference;
478
479/** @brief Encode a ReferenceDescription. */
480void protocore_ua_w_reference(UaWriter *w, const OpcUaReference *ref);
481
482/** @brief One NodeId the client wants to browse (a BrowseDescription). */
483typedef struct
484{
485 uint16_t ns;
486 uint32_t id;
487 proto_bool numeric;
488} OpcUaBrowseItem;
489
490/** @brief Parsed BrowseRequest: the MSG envelope plus the (bounded) NodesToBrowse list. */
491typedef struct
492{
493 OpcUaMsg msg;
494 uint32_t total;
495 uint32_t count;
496 OpcUaBrowseItem items[PROTOCORE_OPCUA_BROWSE_MAX];
497} OpcUaBrowseRequest;
498
499/** @brief Parse a `MSG` BrowseRequest. @return true if valid. */
500proto_bool protocore_opcua_parse_browse(const uint8_t *msg, size_t len, OpcUaBrowseRequest *out);
501
502/**
503 * @brief Application Browse resolver: write up to @p max references for (ns, id) into
504 * @p out. @return the count written, or -1 for an unknown node (the server
505 * answers BadNodeIdUnknown for that BrowseResult).
506 */
507typedef int32_t (*OpcUaBrowseHandler)(uint16_t ns, uint32_t id, OpcUaReference *out, uint32_t max);
508
509/** @brief Register the Browse resolver the ProtoConn::PROTO_OPCUA server calls for each browsed node. */
510void protocore_opcua_set_browse_handler(OpcUaBrowseHandler fn);
511
512/** @brief The Browse resolver registered above, or null while none is. */
513OpcUaBrowseHandler protocore_opcua_browse_handler(void);
514
515/**
516 * @brief Build a `MSG` BrowseResponse: one BrowseResult per browsed node, each with the
517 * references the @p handler returns (up to PROTOCORE_OPCUA_REF_MAX).
518 * @return total MSG bytes written, or 0 if it does not fit @p cap.
519 */
520size_t protocore_opcua_build_browse_response(const OpcUaBrowseRequest *req, OpcUaBrowseHandler handler, uint32_t seq,
521 int64_t now_ft, uint8_t *out, size_t cap);
522
523/** @brief Build a `MSG` CloseSessionResponse (ResponseHeader only, ServiceResult Good). */
524size_t protocore_opcua_build_close_session_response(const OpcUaMsg *req, uint32_t seq, int64_t now_ft, uint8_t *out,
525 size_t cap);
526
527// ---------------------------------------------------------------------------
528// GetEndpoints + ServiceFault (third-party client interop)
529// ---------------------------------------------------------------------------
530
531/** @brief Numeric NodeIds (namespace 0) for GetEndpoints / ServiceFault (binary encoding ids). */
532#define OPCUA_ID_GET_ENDPOINTS_REQ 428 ///< GetEndpointsRequest_Encoding_DefaultBinary.
533#define OPCUA_ID_GET_ENDPOINTS_RESP 431 ///< GetEndpointsResponse_Encoding_DefaultBinary.
534#define OPCUA_ID_SERVICE_FAULT 397 ///< ServiceFault_Encoding_DefaultBinary.
535
536/** @brief StatusCode for an unsupported service (returned in a ServiceFault). */
537#define OPCUA_STATUS_BAD_SERVICE_UNSUPPORTED 0x800B0000u
538
539/** @brief Encode one EndpointDescription (SecurityMode/Policy None, one Anonymous user-token policy). */
540void protocore_ua_w_endpoint_description(UaWriter *w, const OpcUaServerInfo *info);
541
542/** @brief Build a `MSG` GetEndpointsResponse advertising a single SecurityPolicy None endpoint. */
543size_t protocore_opcua_build_get_endpoints_response(const OpcUaMsg *req, const OpcUaServerInfo *info, uint32_t seq,
544 int64_t now_ft, uint8_t *out, size_t cap);
545
546/**
547 * @brief Build a `MSG` ServiceFault (TypeId i=397) - a bare ResponseHeader carrying
548 * @p service_result, the reply for an unsupported/unknown service request.
549 */
550size_t protocore_opcua_build_service_fault(const OpcUaMsg *req, uint32_t service_result, uint32_t seq, int64_t now_ft,
551 uint8_t *out, size_t cap);
552
553/** @brief Set the endpoint URL the ProtoConn::PROTO_OPCUA server advertises (GetEndpoints / CreateSession). */
554void protocore_opcua_set_endpoint_url(const char *url);
555
556// ---------------------------------------------------------------------------
557// Write service (MSG, SecurityPolicy None)
558// ---------------------------------------------------------------------------
559
560/** @brief Numeric NodeIds (namespace 0) for the Write service (binary encoding ids). */
561#define OPCUA_ID_WRITE_REQ 673 ///< WriteRequest_Encoding_DefaultBinary.
562#define OPCUA_ID_WRITE_RESP 676 ///< WriteResponse_Encoding_DefaultBinary.
563
564/** @brief StatusCode for a node/attribute that cannot be written. */
565#define OPCUA_STATUS_BAD_NOT_WRITABLE 0x803B0000u
566
567/** @brief One value the client wants to write (a WriteValue). */
568typedef struct
569{
570 uint16_t ns;
571 uint32_t id;
572 proto_bool numeric;
573 uint32_t attribute;
574 OpcUaVariant value; ///< the DataValue's Variant (string values point into the source buffer).
575} OpcUaWriteItem;
576
577/** @brief Parsed WriteRequest: the MSG envelope plus the (bounded) NodesToWrite list. */
578typedef struct
579{
580 OpcUaMsg msg;
581 uint32_t total;
582 uint32_t count;
583 OpcUaWriteItem items[PROTOCORE_OPCUA_WRITE_MAX];
584} OpcUaWriteRequest;
585
586/** @brief Parse a `MSG` WriteRequest. @return true if valid. */
587proto_bool protocore_opcua_parse_write(const uint8_t *msg, size_t len, OpcUaWriteRequest *out);
588
589/**
590 * @brief Build a `MSG` WriteResponse: one StatusCode per NodesToWrite entry.
591 * @param results per-node StatusCodes (0 = Good); may be null (all Good).
592 * @return total MSG bytes written, or 0 if it does not fit @p cap.
593 */
594size_t protocore_opcua_build_write_response(const OpcUaWriteRequest *req, const uint32_t *results, uint32_t seq,
595 int64_t now_ft, uint8_t *out, size_t cap);
596
597/**
598 * @brief Application Write resolver: apply @p value to (ns, id, attribute) and return a
599 * StatusCode (0 = Good). Return a Bad code (e.g. OPCUA_STATUS_BAD_NODE_ID_UNKNOWN
600 * or OPCUA_STATUS_BAD_NOT_WRITABLE) to reject the write.
601 */
602typedef uint32_t (*OpcUaWriteHandler)(uint16_t ns, uint32_t id, uint32_t attribute, const OpcUaVariant *value);
603
604/** @brief Register the Write resolver the ProtoConn::PROTO_OPCUA server calls for each WriteRequest node. */
605void protocore_opcua_set_write_handler(OpcUaWriteHandler fn);
606
607// ---------------------------------------------------------------------------
608// ESP32 TCP server (ProtoConn::PROTO_OPCUA data handler)
609// ---------------------------------------------------------------------------
610
611/**
612 * @brief ProtoConn::PROTO_OPCUA data handler: handshake, SecureChannel, Session, Read.
613 *
614 * Dispatched by the session layer for connections accepted on an OPC UA listener
615 * (`listen(4840, ProtoConn::PROTO_OPCUA)`). Drains framed messages from the slot's rx ring:
616 * `HEL` -> negotiated `ACK`, `OPN` -> OpenSecureChannelResponse (SecurityPolicy
617 * None), `MSG` GetEndpoints/CreateSession/ActivateSession/Read/Write/Browse/
618 * CloseSession -> their responses (Read/Write/Browse call the registered resolvers;
619 * an unknown service draws a ServiceFault), `CLO` (CloseSecureChannel) -> close.
620 */
621void protocore_opcua_rx(uint8_t slot);
622
623/** @brief The OPC UA ProtoHandler (accessor; nullptr on host builds; installed by the builtins list). */
624struct ProtoHandler;
625const struct ProtoHandler *protocore_opcua_protocore_handler(void);
626
628
629#endif // PROTOCORE_ENABLE_OPCUA
630
631#endif // PROTOCORE_OPCUA_H
PROTO_ENUM_PACKED
Application protocol spoken on a listener port or connection slot.
Per-protocol connection event/poll callbacks (the server's dispatch vtable).
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97