ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
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 (PC_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; pc_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 PROTOCORE_OPCUA_H
52#define PROTOCORE_OPCUA_H
53
54#include "protocore_config.h"
55#include <stddef.h>
56#include <stdint.h>
57
58#if PC_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 pc_ua_w_u8(UaWriter *w, uint8_t v);
73void pc_ua_w_u16(UaWriter *w, uint16_t v);
74void pc_ua_w_u32(UaWriter *w, uint32_t v);
75void pc_ua_w_u64(UaWriter *w, uint64_t v);
76void pc_ua_w_i32(UaWriter *w, int32_t v);
77void pc_ua_w_f32(UaWriter *w, float v);
78void pc_ua_w_f64(UaWriter *w, double v);
79void pc_ua_w_bool(UaWriter *w, bool v);
80/** @brief Encode a String/ByteString: int32 length (-1 = null) then the bytes. */
81void pc_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 pc_ua_r_u8(UaReader *r);
92uint16_t pc_ua_r_u16(UaReader *r);
93uint32_t pc_ua_r_u32(UaReader *r);
94uint64_t pc_ua_r_u64(UaReader *r);
95int32_t pc_ua_r_i32(UaReader *r);
96float pc_ua_r_f32(UaReader *r);
97double pc_ua_r_f64(UaReader *r);
98bool pc_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 pc_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 pc_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 pc_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 PC_OPCUA_BUF limit.
141 * @return total ACK message bytes written to @p out, or 0 if it does not fit.
142 */
143size_t pc_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 PC_OPCUA_BAD_TCP_MESSAGE_TYPE_INVALID 0x807E0000u
147#define PC_OPCUA_BAD_TCP_MESSAGE_TOO_LARGE 0x80800000u
148#define PC_OPCUA_BAD_TCP_NOT_ENOUGH_RESOURCES 0x80810000u
149#define PC_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 * (PC_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 pc_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). */
164struct UaNodeId
165{
166 uint16_t ns; ///< NamespaceIndex.
167 uint32_t id; ///< Numeric identifier.
168 bool numeric; ///< false for a String/Guid/ByteString id (value skipped on read).
169};
170
171/** @brief Encode a numeric NodeId, picking the smallest of the TwoByte/FourByte/Numeric forms. */
172void pc_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 */
178bool pc_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 pc_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. */
195struct OpcUaOpenChannel
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};
206
207/** @brief Parse a complete `OPN` message (SecurityPolicy None). @return true if valid. */
208bool pc_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 pc_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, size_t cap);
222
223// ---------------------------------------------------------------------------
224// Session - CreateSession / ActivateSession (MSG service calls, SecurityPolicy None)
225// ---------------------------------------------------------------------------
226
227/** @brief Numeric NodeIds (namespace 0) for the Session services (binary encoding ids). */
228#define OPCUA_ID_CREATE_SESSION_REQ 461 ///< CreateSessionRequest_Encoding_DefaultBinary.
229#define OPCUA_ID_CREATE_SESSION_RESP 464 ///< CreateSessionResponse_Encoding_DefaultBinary.
230#define OPCUA_ID_ACTIVATE_SESSION_REQ 467 ///< ActivateSessionRequest_Encoding_DefaultBinary.
231#define OPCUA_ID_ACTIVATE_SESSION_RESP 470 ///< ActivateSessionResponse_Encoding_DefaultBinary.
232
233/** @brief Common fields of a `MSG` (secure conversation) service request. */
234struct OpcUaMsg
235{
236 uint32_t secure_channel_id; ///< SecureChannelId (echoed in the response).
237 uint32_t token_id; ///< SymmetricSecurityHeader TokenId.
238 uint32_t sequence_number; ///< SequenceHeader SequenceNumber.
239 uint32_t request_id; ///< SequenceHeader RequestId (echoed in the response).
240 uint32_t type_id; ///< Body TypeId NodeId (numeric id; 0 if non-numeric).
241 uint32_t request_handle; ///< RequestHeader RequestHandle (echoed in the response).
242};
243
244/**
245 * @brief Parse a `MSG` envelope: security + sequence headers, body TypeId, and the
246 * leading RequestHeader (every service request starts with one).
247 * @return true if valid. @p out->type_id selects the service to dispatch.
248 */
249bool pc_opcua_parse_msg(const uint8_t *msg, size_t len, OpcUaMsg *out);
250
251/** @brief Identity the server advertises in GetEndpoints / CreateSession endpoint descriptions. */
252struct OpcUaServerInfo
253{
254 const char *endpoint_url; ///< e.g. "opc.tcp://192.168.1.85:4840".
255 const char *application_uri; ///< server ApplicationUri.
256 const char *application_name; ///< server ApplicationName (display text).
257};
258
259/**
260 * @brief Build a `MSG` CreateSessionResponse (SecurityPolicy None): assign a SessionId
261 * and AuthenticationToken and advertise one None endpoint in ServerEndpoints so
262 * a client's endpoint validation matches GetEndpoints.
263 * @param req the parsed request (TokenId/RequestId/RequestHandle echoed).
264 * @param session_id numeric SessionId identifier the server assigns (namespace 1).
265 * @param auth_token numeric AuthenticationToken the server assigns (namespace 1).
266 * @param revised_timeout RevisedSessionTimeout (ms).
267 * @param info server identity for the advertised endpoint (may be null for defaults).
268 * @param seq server SequenceNumber for this message.
269 * @param now_ft OPC UA DateTime for the response timestamp (0 = unset).
270 * @return total MSG bytes written, or 0 if it does not fit @p cap.
271 */
272size_t pc_opcua_build_create_session_response(const OpcUaMsg *req, uint32_t session_id, uint32_t auth_token,
273 double revised_timeout, const OpcUaServerInfo *info, uint32_t seq,
274 int64_t now_ft, uint8_t *out, size_t cap);
275
276/**
277 * @brief Build a `MSG` ActivateSessionResponse (SecurityPolicy None): ServiceResult Good,
278 * empty ServerNonce / Results / DiagnosticInfos.
279 * @return total MSG bytes written, or 0 if it does not fit @p cap.
280 */
281size_t pc_opcua_build_activate_session_response(const OpcUaMsg *req, uint32_t seq, int64_t now_ft, uint8_t *out,
282 size_t cap);
283
284// ---------------------------------------------------------------------------
285// Read service (MSG, SecurityPolicy None) + Variant / DataValue encoding
286// ---------------------------------------------------------------------------
287
288/** @brief Numeric NodeIds (namespace 0) for the Read service (binary encoding ids). */
289#define OPCUA_ID_READ_REQ 631 ///< ReadRequest_Encoding_DefaultBinary.
290#define OPCUA_ID_READ_RESP 634 ///< ReadResponse_Encoding_DefaultBinary.
291
292/** @brief The OPC UA Attribute id a Read usually wants (the node's Value). */
293#define OPCUA_ATTR_VALUE 13
294
295/** @brief StatusCodes the Read service returns. */
296#define OPCUA_STATUS_GOOD 0x00000000u
297#define OPCUA_STATUS_BAD_NODE_ID_UNKNOWN 0x80340000u
298
299/** @brief OPC UA built-in type ids for the scalar Variants the Read service encodes. */
300enum class OpcUaVariantType : uint8_t
301{
302 OPCUA_VAR_NULL = 0,
303 OPCUA_VAR_BOOL = 1,
304 OPCUA_VAR_INT32 = 6,
305 OPCUA_VAR_UINT32 = 7,
306 OPCUA_VAR_INT64 = 8,
307 OPCUA_VAR_UINT64 = 9,
308 OPCUA_VAR_FLOAT = 10,
309 OPCUA_VAR_DOUBLE = 11,
310 OPCUA_VAR_STRING = 12,
311};
312
313/** @brief A scalar OPC UA Variant value (the supported built-in types). */
314struct OpcUaVariant
315{
316 OpcUaVariantType type; ///< 0 = null Variant.
317 bool b; ///< OpcUaVariantType::OPCUA_VAR_BOOL.
318 int32_t i32; ///< OpcUaVariantType::OPCUA_VAR_INT32.
319 uint32_t u32; ///< OpcUaVariantType::OPCUA_VAR_UINT32.
320 int64_t i64; ///< OpcUaVariantType::OPCUA_VAR_INT64.
321 uint64_t u64; ///< OpcUaVariantType::OPCUA_VAR_UINT64.
322 float f32; ///< OpcUaVariantType::OPCUA_VAR_FLOAT.
323 double f64; ///< OpcUaVariantType::OPCUA_VAR_DOUBLE.
324 const char *str; ///< OpcUaVariantType::OPCUA_VAR_STRING (referenced, not copied).
325 int32_t str_len; ///< string length (-1 = null string).
326};
327
328/** @brief Encode a scalar Variant: encoding byte (built-in type id) then the value. */
329void pc_ua_w_variant(UaWriter *w, const OpcUaVariant *v);
330
331/** @brief Encode a DataValue: a value/status mask byte then the Variant and/or StatusCode. */
332void pc_ua_w_datavalue(UaWriter *w, const OpcUaVariant *v, uint32_t status);
333
334/**
335 * @brief Decode a scalar Variant. A decoded OpcUaVariantType::OPCUA_VAR_STRING points into the source
336 * buffer (keep it alive). Non-scalar/array Variants latch @c err.
337 */
338bool pc_ua_r_variant(UaReader *r, OpcUaVariant *out);
339
340/**
341 * @brief Decode a DataValue: the mask byte, then (if present) the Variant value and
342 * StatusCode; SourceTimestamp/ServerTimestamp (and picoseconds) are skipped.
343 * @param out_value filled with the value (type 0 if no value field present).
344 * @param out_status set to the StatusCode (0 if not present).
345 */
346bool pc_ua_r_datavalue(UaReader *r, OpcUaVariant *out_value, uint32_t *out_status);
347
348/** @brief One NodeId + attribute the client wants to read (a ReadValueId). */
349struct OpcUaReadItem
350{
351 uint16_t ns;
352 uint32_t id;
353 bool numeric;
354 uint32_t attribute;
355};
356
357/** @brief Parsed ReadRequest: the MSG envelope plus the (bounded) NodesToRead list. */
358struct OpcUaReadRequest
359{
360 OpcUaMsg msg; ///< envelope + RequestHeader (type_id = ReadRequest).
361 uint32_t total; ///< nodes requested (may exceed the captured count).
362 uint32_t count; ///< nodes captured (clamped to PC_OPCUA_READ_MAX).
363 OpcUaReadItem items[PC_OPCUA_READ_MAX];
364};
365
366/** @brief Parse a `MSG` ReadRequest. @return true if valid. */
367bool pc_opcua_parse_read(const uint8_t *msg, size_t len, OpcUaReadRequest *out);
368
369/**
370 * @brief Build a `MSG` ReadResponse: one DataValue per captured NodesToRead entry.
371 * @param values per-node values (values[i] paired with req->items[i]); null type = no value.
372 * @param statuses per-node StatusCodes (0 = Good).
373 * @return total MSG bytes written, or 0 if it does not fit @p cap.
374 */
375size_t pc_opcua_build_read_response(const OpcUaReadRequest *req, const OpcUaVariant *values, const uint32_t *statuses,
376 uint32_t seq, int64_t now_ft, uint8_t *out, size_t cap);
377
378/**
379 * @brief Application Read resolver: fill @p out for (ns, id, attribute). Return false
380 * for an unknown node/attribute (the server answers BadNodeIdUnknown).
381 */
382typedef bool (*OpcUaReadHandler)(uint16_t ns, uint32_t id, uint32_t attribute, OpcUaVariant *out);
383
384/** @brief Register the Read resolver the ConnProto::PROTO_OPCUA server calls for each ReadRequest node. */
385void pc_opcua_set_read_handler(OpcUaReadHandler fn);
386
387// ---------------------------------------------------------------------------
388// Browse service + Close (MSG, SecurityPolicy None)
389// ---------------------------------------------------------------------------
390
391/** @brief Numeric NodeIds (namespace 0) for Browse / Close services (binary encoding ids). */
392#define OPCUA_ID_BROWSE_REQ 527 ///< BrowseRequest_Encoding_DefaultBinary.
393#define OPCUA_ID_BROWSE_RESP 530 ///< BrowseResponse_Encoding_DefaultBinary.
394#define OPCUA_ID_CLOSE_SESSION_REQ 473 ///< CloseSessionRequest_Encoding_DefaultBinary.
395#define OPCUA_ID_CLOSE_SESSION_RESP 476 ///< CloseSessionResponse_Encoding_DefaultBinary.
396
397/** @brief Common NodeClass / ReferenceType / TypeDefinition ids (namespace 0) for Browse results. */
398#define OPCUA_NODECLASS_OBJECT 1
399#define OPCUA_NODECLASS_VARIABLE 2
400#define OPCUA_REFTYPE_ORGANIZES 35
401#define OPCUA_REFTYPE_HAS_COMPONENT 47
402#define OPCUA_TYPEDEF_BASE_OBJECT 58
403#define OPCUA_TYPEDEF_BASE_DATA_VARIABLE 63
404
405/** @brief Encode a QualifiedName: NamespaceIndex (UInt16) + Name (String). */
406void pc_ua_w_qualifiedname(UaWriter *w, uint16_t ns, const char *name);
407
408/** @brief Encode a LocalizedText: a present-fields mask then the optional Locale / Text Strings. */
409void pc_ua_w_localizedtext(UaWriter *w, const char *locale, const char *text);
410
411/** @brief One reference (ReferenceDescription) returned by a Browse. Strings are referenced, not copied. */
412struct OpcUaReference
413{
414 uint32_t ref_type_id; ///< ReferenceType NodeId numeric id (e.g. OPCUA_REFTYPE_ORGANIZES).
415 bool is_forward; ///< IsForward.
416 uint16_t target_ns; ///< target NodeId namespace.
417 uint32_t target_id; ///< target NodeId numeric id.
418 uint16_t browse_name_ns; ///< BrowseName namespace.
419 const char *browse_name; ///< BrowseName.Name.
420 const char *display_name; ///< DisplayName text.
421 uint32_t node_class; ///< NodeClass (e.g. OPCUA_NODECLASS_VARIABLE).
422 uint32_t type_def_id; ///< TypeDefinition NodeId numeric id (e.g. OPCUA_TYPEDEF_BASE_DATA_VARIABLE).
423};
424
425/** @brief Encode a ReferenceDescription. */
426void pc_ua_w_reference(UaWriter *w, const OpcUaReference *ref);
427
428/** @brief One NodeId the client wants to browse (a BrowseDescription). */
429struct OpcUaBrowseItem
430{
431 uint16_t ns;
432 uint32_t id;
433 bool numeric;
434};
435
436/** @brief Parsed BrowseRequest: the MSG envelope plus the (bounded) NodesToBrowse list. */
437struct OpcUaBrowseRequest
438{
439 OpcUaMsg msg;
440 uint32_t total;
441 uint32_t count;
442 OpcUaBrowseItem items[PC_OPCUA_BROWSE_MAX];
443};
444
445/** @brief Parse a `MSG` BrowseRequest. @return true if valid. */
446bool pc_opcua_parse_browse(const uint8_t *msg, size_t len, OpcUaBrowseRequest *out);
447
448/**
449 * @brief Application Browse resolver: write up to @p max references for (ns, id) into
450 * @p out. @return the count written, or -1 for an unknown node (the server
451 * answers BadNodeIdUnknown for that BrowseResult).
452 */
453typedef int32_t (*OpcUaBrowseHandler)(uint16_t ns, uint32_t id, OpcUaReference *out, uint32_t max);
454
455/** @brief Register the Browse resolver the ConnProto::PROTO_OPCUA server calls for each browsed node. */
456void pc_opcua_set_browse_handler(OpcUaBrowseHandler fn);
457
458/**
459 * @brief Build a `MSG` BrowseResponse: one BrowseResult per browsed node, each with the
460 * references the @p handler returns (up to PC_OPCUA_REF_MAX).
461 * @return total MSG bytes written, or 0 if it does not fit @p cap.
462 */
463size_t pc_opcua_build_browse_response(const OpcUaBrowseRequest *req, OpcUaBrowseHandler handler, uint32_t seq,
464 int64_t now_ft, uint8_t *out, size_t cap);
465
466/** @brief Build a `MSG` CloseSessionResponse (ResponseHeader only, ServiceResult Good). */
467size_t pc_opcua_build_close_session_response(const OpcUaMsg *req, uint32_t seq, int64_t now_ft, uint8_t *out,
468 size_t cap);
469
470// ---------------------------------------------------------------------------
471// GetEndpoints + ServiceFault (third-party client interop)
472// ---------------------------------------------------------------------------
473
474/** @brief Numeric NodeIds (namespace 0) for GetEndpoints / ServiceFault (binary encoding ids). */
475#define OPCUA_ID_GET_ENDPOINTS_REQ 428 ///< GetEndpointsRequest_Encoding_DefaultBinary.
476#define OPCUA_ID_GET_ENDPOINTS_RESP 431 ///< GetEndpointsResponse_Encoding_DefaultBinary.
477#define OPCUA_ID_SERVICE_FAULT 397 ///< ServiceFault_Encoding_DefaultBinary.
478
479/** @brief StatusCode for an unsupported service (returned in a ServiceFault). */
480#define OPCUA_STATUS_BAD_SERVICE_UNSUPPORTED 0x800B0000u
481
482/** @brief Encode one EndpointDescription (SecurityMode/Policy None, one Anonymous user-token policy). */
483void pc_ua_w_endpoint_description(UaWriter *w, const OpcUaServerInfo *info);
484
485/** @brief Build a `MSG` GetEndpointsResponse advertising a single SecurityPolicy None endpoint. */
486size_t pc_opcua_build_get_endpoints_response(const OpcUaMsg *req, const OpcUaServerInfo *info, uint32_t seq,
487 int64_t now_ft, uint8_t *out, size_t cap);
488
489/**
490 * @brief Build a `MSG` ServiceFault (TypeId i=397) - a bare ResponseHeader carrying
491 * @p service_result, the reply for an unsupported/unknown service request.
492 */
493size_t pc_opcua_build_service_fault(const OpcUaMsg *req, uint32_t service_result, uint32_t seq, int64_t now_ft,
494 uint8_t *out, size_t cap);
495
496/** @brief Set the endpoint URL the ConnProto::PROTO_OPCUA server advertises (GetEndpoints / CreateSession). */
497void pc_opcua_set_endpoint_url(const char *url);
498
499// ---------------------------------------------------------------------------
500// Write service (MSG, SecurityPolicy None)
501// ---------------------------------------------------------------------------
502
503/** @brief Numeric NodeIds (namespace 0) for the Write service (binary encoding ids). */
504#define OPCUA_ID_WRITE_REQ 673 ///< WriteRequest_Encoding_DefaultBinary.
505#define OPCUA_ID_WRITE_RESP 676 ///< WriteResponse_Encoding_DefaultBinary.
506
507/** @brief StatusCode for a node/attribute that cannot be written. */
508#define OPCUA_STATUS_BAD_NOT_WRITABLE 0x803B0000u
509
510/** @brief One value the client wants to write (a WriteValue). */
511struct OpcUaWriteItem
512{
513 uint16_t ns;
514 uint32_t id;
515 bool numeric;
516 uint32_t attribute;
517 OpcUaVariant value; ///< the DataValue's Variant (string values point into the source buffer).
518};
519
520/** @brief Parsed WriteRequest: the MSG envelope plus the (bounded) NodesToWrite list. */
521struct OpcUaWriteRequest
522{
523 OpcUaMsg msg;
524 uint32_t total;
525 uint32_t count;
526 OpcUaWriteItem items[PC_OPCUA_WRITE_MAX];
527};
528
529/** @brief Parse a `MSG` WriteRequest. @return true if valid. */
530bool pc_opcua_parse_write(const uint8_t *msg, size_t len, OpcUaWriteRequest *out);
531
532/**
533 * @brief Build a `MSG` WriteResponse: one StatusCode per NodesToWrite entry.
534 * @param results per-node StatusCodes (0 = Good); may be null (all Good).
535 * @return total MSG bytes written, or 0 if it does not fit @p cap.
536 */
537size_t pc_opcua_build_write_response(const OpcUaWriteRequest *req, const uint32_t *results, uint32_t seq,
538 int64_t now_ft, uint8_t *out, size_t cap);
539
540/**
541 * @brief Application Write resolver: apply @p value to (ns, id, attribute) and return a
542 * StatusCode (0 = Good). Return a Bad code (e.g. OPCUA_STATUS_BAD_NODE_ID_UNKNOWN
543 * or OPCUA_STATUS_BAD_NOT_WRITABLE) to reject the write.
544 */
545typedef uint32_t (*OpcUaWriteHandler)(uint16_t ns, uint32_t id, uint32_t attribute, const OpcUaVariant *value);
546
547/** @brief Register the Write resolver the ConnProto::PROTO_OPCUA server calls for each WriteRequest node. */
548void pc_opcua_set_write_handler(OpcUaWriteHandler fn);
549
550// ---------------------------------------------------------------------------
551// ESP32 TCP server (ConnProto::PROTO_OPCUA data handler)
552// ---------------------------------------------------------------------------
553
554/**
555 * @brief ConnProto::PROTO_OPCUA data handler: handshake, SecureChannel, Session, Read.
556 *
557 * Dispatched by the session layer for connections accepted on an OPC UA listener
558 * (`listen(4840, ConnProto::PROTO_OPCUA)`). Drains framed messages from the slot's rx ring:
559 * `HEL` -> negotiated `ACK`, `OPN` -> OpenSecureChannelResponse (SecurityPolicy
560 * None), `MSG` GetEndpoints/CreateSession/ActivateSession/Read/Write/Browse/
561 * CloseSession -> their responses (Read/Write/Browse call the registered resolvers;
562 * an unknown service draws a ServiceFault), `CLO` (CloseSecureChannel) -> close.
563 */
564void pc_opcua_rx(uint8_t slot);
565
566/** @brief The OPC UA ProtoHandler (accessor; nullptr on host builds; installed by the builtins list). */
567struct ProtoHandler;
568const struct ProtoHandler *pc_opcua_proto_handler(void);
569
570#endif // PC_ENABLE_OPCUA
571#endif // PROTOCORE_OPCUA_H
User-facing configuration for ProtoCore.
Per-protocol connection event/poll callbacks (Layer 5 dispatch vtable).