aboutsummaryrefslogtreecommitdiffstats
path: root/src/nodes_config.c
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-02-19 06:19:23 +0100
committerThomas Vanbesien <tvanbesi@proton.me>2026-02-19 06:19:23 +0100
commit2b632bd229edaa9999be5043f9a8ae2ac7d17e41 (patch)
tree5071ef9fa36a898fbe009f477441fd2f34a4cb2d /src/nodes_config.c
parent37c0fee672afd3701ea3ed87958da4d548bf1be3 (diff)
downloadBobinkCOpcUa-master.tar.gz
BobinkCOpcUa-master.zip
Add configurable variable node initialization for server_registerHEADmaster
New optional CLI argument [nodes-config] lets the server populate its address space from a dot-indexed config file (node.N.name/type/value/ accessLevel/description). Supports 10 scalar types plus 1D arrays.
Diffstat (limited to 'src/nodes_config.c')
-rw-r--r--src/nodes_config.c738
1 files changed, 738 insertions, 0 deletions
diff --git a/src/nodes_config.c b/src/nodes_config.c
new file mode 100644
index 0000000..79d5919
--- /dev/null
+++ b/src/nodes_config.c
@@ -0,0 +1,738 @@
+/**
+ * @file nodes_config.c
+ * @brief Dot-indexed node configuration parser and server-side node creator.
+ */
+
+#include "nodes_config.h"
+#include "config.h"
+
+#include <open62541/plugin/log_stdout.h>
+#include <open62541/server.h>
+#include <open62541/types.h>
+#include <open62541/types_generated.h>
+
+#include <errno.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* ========================================================================
+ * Type Map
+ * ======================================================================== */
+
+typedef struct
+{
+ const char *name;
+ node_type type;
+ int ua_type_index;
+} type_map_entry;
+
+static const type_map_entry _s_type_map[] = {
+ { "bool", NODE_TYPE_BOOL, UA_TYPES_BOOLEAN },
+ { "int16", NODE_TYPE_INT16, UA_TYPES_INT16 },
+ { "uint16", NODE_TYPE_UINT16, UA_TYPES_UINT16 },
+ { "int32", NODE_TYPE_INT32, UA_TYPES_INT32 },
+ { "uint32", NODE_TYPE_UINT32, UA_TYPES_UINT32 },
+ { "int64", NODE_TYPE_INT64, UA_TYPES_INT64 },
+ { "uint64", NODE_TYPE_UINT64, UA_TYPES_UINT64 },
+ { "float", NODE_TYPE_FLOAT, UA_TYPES_FLOAT },
+ { "double", NODE_TYPE_DOUBLE, UA_TYPES_DOUBLE },
+ { "string", NODE_TYPE_STRING, UA_TYPES_STRING },
+};
+
+#define TYPE_MAP_SIZE (sizeof (_s_type_map) / sizeof (_s_type_map[0]))
+
+/* ========================================================================
+ * Static Helpers
+ * ======================================================================== */
+
+/**
+ * @brief Trims leading and trailing whitespace from a string in place.
+ *
+ * Returns a pointer past any leading whitespace and writes a NUL
+ * terminator after the last non-whitespace character.
+ */
+static char *
+_s_trim (char *s)
+{
+ while (*s == ' ' || *s == '\t')
+ s++;
+ size_t len = strlen (s);
+ while (len > 0 && (s[len - 1] == ' ' || s[len - 1] == '\t'))
+ len--;
+ s[len] = '\0';
+ return s;
+}
+
+/**
+ * @brief Resolves a type string to a node_type and array flag.
+ *
+ * Strips a trailing "[]" suffix if present (setting is_array to true),
+ * then looks up the base name in _s_type_map.
+ *
+ * @return 0 on success, -1 if the type name is unknown.
+ */
+static int
+_s_resolve_type (const char *str, node_type *out_type,
+ UA_Boolean *out_is_array)
+{
+ char buf[64];
+ size_t len = strlen (str);
+ if (len >= sizeof (buf))
+ return -1;
+ memcpy (buf, str, len + 1);
+
+ *out_is_array = false;
+ if (len >= 2 && buf[len - 2] == '[' && buf[len - 1] == ']')
+ {
+ buf[len - 2] = '\0';
+ *out_is_array = true;
+ }
+
+ for (size_t i = 0; i < TYPE_MAP_SIZE; i++)
+ {
+ if (strcmp (buf, _s_type_map[i].name) == 0)
+ {
+ *out_type = _s_type_map[i].type;
+ return 0;
+ }
+ }
+ return -1;
+}
+
+/**
+ * @brief Resolves an access level string to a node_access_level enum.
+ *
+ * @return 0 on success, -1 if the string is unknown.
+ */
+static int
+_s_resolve_access (const char *str, node_access_level *out)
+{
+ if (strcmp (str, "read") == 0)
+ {
+ *out = NODE_ACCESS_READ;
+ return 0;
+ }
+ if (strcmp (str, "readwrite") == 0)
+ {
+ *out = NODE_ACCESS_READWRITE;
+ return 0;
+ }
+ return -1;
+}
+
+/**
+ * @brief Returns the UA_TYPES index for a node_type enum value.
+ */
+static int
+_s_ua_type_index (node_type t)
+{
+ for (size_t i = 0; i < TYPE_MAP_SIZE; i++)
+ {
+ if (_s_type_map[i].type == t)
+ return _s_type_map[i].ua_type_index;
+ }
+ return -1;
+}
+
+/**
+ * @brief Scans config entries for the highest N in node.N.* keys.
+ *
+ * @return The maximum index found, or -1 if no node.* keys exist.
+ */
+static int
+_s_find_max_node_index (const config *cfg)
+{
+ int max_idx = -1;
+ for (size_t i = 0; i < cfg->count; i++)
+ {
+ int idx;
+ int consumed;
+ if (sscanf (cfg->entries[i].key, "node.%d.%n", &idx, &consumed) >= 1
+ && consumed > 0 && idx >= 0)
+ {
+ if (idx > max_idx)
+ max_idx = idx;
+ }
+ }
+ return max_idx;
+}
+
+/**
+ * @brief Parses a single scalar value string into a typed buffer.
+ *
+ * For numeric types uses strtol/strtod family with range checks.
+ * For bool accepts "true"/"false". For string creates a UA_String
+ * via UA_String_fromChars (written to out_str).
+ *
+ * @param str The value string to parse.
+ * @param type The target data type.
+ * @param out Buffer to write the parsed value into (must be large
+ * enough for the target type; unused for string).
+ * @param out_str Output for UA_String when type == NODE_TYPE_STRING.
+ * @return 0 on success, -1 on parse error.
+ */
+static int
+_s_parse_scalar_value (const char *str, node_type type, void *out,
+ UA_String *out_str)
+{
+ char *endptr;
+ errno = 0;
+
+ switch (type)
+ {
+ case NODE_TYPE_BOOL:
+ if (strcmp (str, "true") == 0)
+ *(UA_Boolean *)out = true;
+ else if (strcmp (str, "false") == 0)
+ *(UA_Boolean *)out = false;
+ else
+ return -1;
+ return 0;
+
+ case NODE_TYPE_INT16:
+ {
+ long v = strtol (str, &endptr, 10);
+ if (*endptr != '\0' || errno == ERANGE || v < INT16_MIN
+ || v > INT16_MAX)
+ return -1;
+ *(UA_Int16 *)out = (UA_Int16)v;
+ return 0;
+ }
+
+ case NODE_TYPE_UINT16:
+ {
+ unsigned long v = strtoul (str, &endptr, 10);
+ if (*endptr != '\0' || errno == ERANGE || v > UINT16_MAX)
+ return -1;
+ *(UA_UInt16 *)out = (UA_UInt16)v;
+ return 0;
+ }
+
+ case NODE_TYPE_INT32:
+ {
+ long v = strtol (str, &endptr, 10);
+ if (*endptr != '\0' || errno == ERANGE || v < INT32_MIN
+ || v > INT32_MAX)
+ return -1;
+ *(UA_Int32 *)out = (UA_Int32)v;
+ return 0;
+ }
+
+ case NODE_TYPE_UINT32:
+ {
+ unsigned long v = strtoul (str, &endptr, 10);
+ if (*endptr != '\0' || errno == ERANGE || v > UINT32_MAX)
+ return -1;
+ *(UA_UInt32 *)out = (UA_UInt32)v;
+ return 0;
+ }
+
+ case NODE_TYPE_INT64:
+ {
+ long long v = strtoll (str, &endptr, 10);
+ if (*endptr != '\0' || errno == ERANGE)
+ return -1;
+ *(UA_Int64 *)out = (UA_Int64)v;
+ return 0;
+ }
+
+ case NODE_TYPE_UINT64:
+ {
+ unsigned long long v = strtoull (str, &endptr, 10);
+ if (*endptr != '\0' || errno == ERANGE)
+ return -1;
+ *(UA_UInt64 *)out = (UA_UInt64)v;
+ return 0;
+ }
+
+ case NODE_TYPE_FLOAT:
+ {
+ float v = strtof (str, &endptr);
+ if (*endptr != '\0' || errno == ERANGE)
+ return -1;
+ *(UA_Float *)out = (UA_Float)v;
+ return 0;
+ }
+
+ case NODE_TYPE_DOUBLE:
+ {
+ double v = strtod (str, &endptr);
+ if (*endptr != '\0' || errno == ERANGE)
+ return -1;
+ *(UA_Double *)out = (UA_Double)v;
+ return 0;
+ }
+
+ case NODE_TYPE_STRING:
+ *out_str = UA_String_fromChars (str);
+ return 0;
+ }
+
+ return -1;
+}
+
+/**
+ * @brief Counts the number of comma-separated elements in a string.
+ */
+static size_t
+_s_count_elements (const char *str)
+{
+ if (*str == '\0')
+ return 0;
+ size_t count = 1;
+ for (const char *p = str; *p; p++)
+ {
+ if (*p == ',')
+ count++;
+ }
+ return count;
+}
+
+/**
+ * @brief Parses a comma-separated value string into a UA array.
+ *
+ * Tokenizes by comma, trims whitespace, parses each element with
+ * _s_parse_scalar_value, and writes into a heap-allocated array.
+ *
+ * @param str The comma-separated value string.
+ * @param type The node data type.
+ * @param ua_type_index The UA_TYPES index for the data type.
+ * @param out_array Output: heap-allocated array (free with
+ * UA_Array_delete).
+ * @param out_size Output: number of elements.
+ * @return 0 on success, -1 on error.
+ */
+static int
+_s_parse_array_value (const char *str, node_type type, int ua_type_index,
+ void **out_array, size_t *out_size)
+{
+ size_t count = _s_count_elements (str);
+ if (count == 0)
+ {
+ *out_array = NULL;
+ *out_size = 0;
+ return 0;
+ }
+
+ const UA_DataType *dt = &UA_TYPES[ua_type_index];
+ void *array = UA_Array_new (count, dt);
+ if (!array)
+ return -1;
+
+ char *tmp = strdup (str);
+ if (!tmp)
+ {
+ UA_Array_delete (array, count, dt);
+ return -1;
+ }
+
+ size_t i = 0;
+ char *saveptr;
+ char *token = strtok_r (tmp, ",", &saveptr);
+
+ while (token && i < count)
+ {
+ char *trimmed = _s_trim (token);
+ if (*trimmed == '\0')
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "NodesConfig: empty element at position %zu", i);
+ goto fail;
+ }
+
+ /* Parse into the array element at offset i. */
+ void *elem = (char *)array + i * dt->memSize;
+ UA_String str_val = UA_STRING_NULL;
+
+ if (_s_parse_scalar_value (trimmed, type, elem, &str_val) != 0)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "NodesConfig: failed to parse array element '%s'",
+ trimmed);
+ goto fail;
+ }
+
+ /* For strings, copy into the array slot. */
+ if (type == NODE_TYPE_STRING)
+ *(UA_String *)elem = str_val;
+
+ i++;
+ token = strtok_r (NULL, ",", &saveptr);
+ }
+
+ free (tmp);
+ *out_array = array;
+ *out_size = count;
+ return 0;
+
+fail:
+ free (tmp);
+ UA_Array_delete (array, count, dt);
+ return -1;
+}
+
+/* ========================================================================
+ * Parsing: nodes_config_load
+ * ======================================================================== */
+
+/**
+ * @brief Frees a partially populated raw node array.
+ */
+static void
+_s_free_raw_nodes (node_config *raw, size_t count)
+{
+ for (size_t i = 0; i < count; i++)
+ {
+ free (raw[i].name);
+ free (raw[i].description);
+ free (raw[i].value_str);
+ }
+ free (raw);
+}
+
+int
+nodes_config_load (const char *path, nodes_config *out)
+{
+ memset (out, 0, sizeof (*out));
+
+ config cfg = { 0 };
+ if (config_load (path, &cfg) != 0)
+ return -1;
+
+ int max_idx = _s_find_max_node_index (&cfg);
+ if (max_idx < 0)
+ {
+ /* No node.* keys — empty config, not an error. */
+ config_free (&cfg);
+ return 0;
+ }
+
+ size_t raw_count = (size_t)(max_idx + 1);
+ node_config *raw = calloc (raw_count, sizeof (node_config));
+ if (!raw)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "NodesConfig: out of memory");
+ config_free (&cfg);
+ return -1;
+ }
+
+ /* Track which fields have been set to detect unknown fields and
+ distinguish "not set" from "set to default enum value 0". */
+ UA_Boolean *type_set = calloc (raw_count, sizeof (UA_Boolean));
+ UA_Boolean *access_set = calloc (raw_count, sizeof (UA_Boolean));
+ if (!type_set || !access_set)
+ {
+ free (type_set);
+ free (access_set);
+ _s_free_raw_nodes (raw, raw_count);
+ config_free (&cfg);
+ return -1;
+ }
+
+ int rc = 0;
+
+ for (size_t i = 0; i < cfg.count; i++)
+ {
+ int idx;
+ int consumed = 0;
+ if (sscanf (cfg.entries[i].key, "node.%d.%n", &idx, &consumed) < 1
+ || consumed == 0 || idx < 0 || (size_t)idx >= raw_count)
+ continue;
+
+ const char *field = cfg.entries[i].key + consumed;
+ const char *value = cfg.entries[i].value;
+
+ if (strcmp (field, "name") == 0)
+ {
+ free (raw[idx].name);
+ raw[idx].name = strdup (value);
+ }
+ else if (strcmp (field, "description") == 0)
+ {
+ free (raw[idx].description);
+ raw[idx].description = strdup (value);
+ }
+ else if (strcmp (field, "type") == 0)
+ {
+ if (_s_resolve_type (value, &raw[idx].type, &raw[idx].is_array) != 0)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "NodesConfig: unknown type '%s' for node.%d",
+ value, idx);
+ rc = -1;
+ goto done;
+ }
+ type_set[idx] = true;
+ }
+ else if (strcmp (field, "value") == 0)
+ {
+ free (raw[idx].value_str);
+ raw[idx].value_str = strdup (value);
+ }
+ else if (strcmp (field, "accessLevel") == 0)
+ {
+ if (_s_resolve_access (value, &raw[idx].access) != 0)
+ {
+ UA_LOG_ERROR (
+ UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "NodesConfig: unknown accessLevel '%s' for node.%d "
+ "(expected 'read' or 'readwrite')",
+ value, idx);
+ rc = -1;
+ goto done;
+ }
+ access_set[idx] = true;
+ }
+ else
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "NodesConfig: unknown field '%s' for node.%d", field,
+ idx);
+ rc = -1;
+ goto done;
+ }
+ }
+
+ /* Compact: count populated entries (those with a name). */
+ size_t populated = 0;
+ for (size_t i = 0; i < raw_count; i++)
+ {
+ if (raw[i].name)
+ populated++;
+ }
+
+ if (populated == 0)
+ {
+ /* All indices were gaps. */
+ goto done;
+ }
+
+ /* Validate required fields for each populated node. */
+ for (size_t i = 0; i < raw_count; i++)
+ {
+ if (!raw[i].name)
+ continue;
+
+ if (!type_set[i])
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "NodesConfig: node.%zu missing required key 'type'",
+ i);
+ rc = -1;
+ goto done;
+ }
+ if (!raw[i].value_str)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "NodesConfig: node.%zu missing required key 'value'",
+ i);
+ rc = -1;
+ goto done;
+ }
+ if (!access_set[i])
+ {
+ UA_LOG_ERROR (
+ UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "NodesConfig: node.%zu missing required key 'accessLevel'", i);
+ rc = -1;
+ goto done;
+ }
+ }
+
+ /* Build the compacted output array. */
+ out->nodes = calloc (populated, sizeof (node_config));
+ if (!out->nodes)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "NodesConfig: out of memory");
+ rc = -1;
+ goto done;
+ }
+
+ size_t j = 0;
+ for (size_t i = 0; i < raw_count; i++)
+ {
+ if (!raw[i].name)
+ continue;
+ out->nodes[j] = raw[i];
+ /* Transfer ownership: clear the raw slot so _s_free_raw_nodes
+ does not double-free these strings. */
+ memset (&raw[i], 0, sizeof (node_config));
+ j++;
+ }
+ out->count = populated;
+
+done:
+ free (type_set);
+ free (access_set);
+ _s_free_raw_nodes (raw, raw_count);
+ config_free (&cfg);
+ if (rc != 0)
+ nodes_config_free (out);
+ return rc;
+}
+
+/* ========================================================================
+ * Node Creation: nodes_config_add
+ * ======================================================================== */
+
+int
+nodes_config_add (UA_Server *server, const nodes_config *nc)
+{
+ int failures = 0;
+
+ for (size_t i = 0; i < nc->count; i++)
+ {
+ const node_config *n = &nc->nodes[i];
+ int ua_idx = _s_ua_type_index (n->type);
+ if (ua_idx < 0)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_SERVER,
+ "NodesConfig: internal error resolving type for '%s'",
+ n->name);
+ failures++;
+ continue;
+ }
+
+ const UA_DataType *dt = &UA_TYPES[ua_idx];
+
+ UA_VariableAttributes attr = UA_VariableAttributes_default;
+ attr.displayName = UA_LOCALIZEDTEXT ("en-US", n->name);
+ if (n->description)
+ attr.description = UA_LOCALIZEDTEXT ("en-US", n->description);
+ attr.dataType = dt->typeId;
+
+ if (n->access == NODE_ACCESS_READ)
+ attr.accessLevel = UA_ACCESSLEVELMASK_READ;
+ else
+ attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
+
+ /* Parse value and set the variant. */
+ UA_Boolean value_ok = false;
+
+ /* Union large enough for any scalar type. */
+ union
+ {
+ UA_Boolean b;
+ UA_Int16 i16;
+ UA_UInt16 u16;
+ UA_Int32 i32;
+ UA_UInt32 u32;
+ UA_Int64 i64;
+ UA_UInt64 u64;
+ UA_Float f;
+ UA_Double d;
+ } scalar_buf;
+ UA_String string_buf = UA_STRING_NULL;
+ void *array_buf = NULL;
+ size_t array_size = 0;
+ UA_UInt32 array_dim = 0;
+
+ if (!n->is_array)
+ {
+ /* Scalar */
+ if (_s_parse_scalar_value (n->value_str, n->type, &scalar_buf,
+ &string_buf)
+ != 0)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_SERVER,
+ "NodesConfig: failed to parse value '%s' "
+ "for node '%s'",
+ n->value_str, n->name);
+ failures++;
+ continue;
+ }
+
+ if (n->type == NODE_TYPE_STRING)
+ UA_Variant_setScalar (&attr.value, &string_buf, dt);
+ else
+ UA_Variant_setScalar (&attr.value, &scalar_buf, dt);
+
+ attr.valueRank = UA_VALUERANK_SCALAR;
+ value_ok = true;
+ }
+ else
+ {
+ /* Array */
+ if (_s_parse_array_value (n->value_str, n->type, ua_idx, &array_buf,
+ &array_size)
+ != 0)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_SERVER,
+ "NodesConfig: failed to parse array value "
+ "for node '%s'",
+ n->name);
+ failures++;
+ continue;
+ }
+
+ UA_Variant_setArray (&attr.value, array_buf, array_size, dt);
+ attr.valueRank = UA_VALUERANK_ONE_DIMENSION;
+ array_dim = (UA_UInt32)array_size;
+ attr.arrayDimensions = &array_dim;
+ attr.arrayDimensionsSize = 1;
+ value_ok = true;
+ }
+
+ if (!value_ok)
+ {
+ failures++;
+ continue;
+ }
+
+ UA_NodeId node_id = UA_NODEID_STRING (1, n->name);
+ UA_QualifiedName browse_name = UA_QUALIFIEDNAME (1, n->name);
+
+ UA_StatusCode rv = UA_Server_addVariableNode (
+ server, node_id, UA_NODEID_NUMERIC (0, UA_NS0ID_OBJECTSFOLDER),
+ UA_NODEID_NUMERIC (0, UA_NS0ID_ORGANIZES), browse_name,
+ UA_NODEID_NUMERIC (0, UA_NS0ID_BASEDATAVARIABLETYPE), attr, NULL,
+ NULL);
+
+ if (rv != UA_STATUSCODE_GOOD)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_SERVER,
+ "NodesConfig: failed to add node '%s': %s", n->name,
+ UA_StatusCode_name (rv));
+ failures++;
+ }
+ else
+ {
+ UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_SERVER,
+ "NodesConfig: added node '%s' "
+ "(ns=1;s=%s, %s%s, %s)",
+ n->name, n->name, _s_type_map[n->type].name,
+ n->is_array ? "[]" : "",
+ n->access == NODE_ACCESS_READ ? "read" : "readwrite");
+ }
+
+ /* Clean up parsed values. UA_Server_addVariableNode deep-copies
+ everything, so our local buffers can be freed now. */
+ if (n->type == NODE_TYPE_STRING && !n->is_array)
+ UA_String_clear (&string_buf);
+ if (array_buf)
+ UA_Array_delete (array_buf, array_size, dt);
+ }
+
+ return failures > 0 ? -1 : 0;
+}
+
+/* ========================================================================
+ * Cleanup
+ * ======================================================================== */
+
+void
+nodes_config_free (nodes_config *nc)
+{
+ for (size_t i = 0; i < nc->count; i++)
+ {
+ free (nc->nodes[i].name);
+ free (nc->nodes[i].description);
+ free (nc->nodes[i].value_str);
+ }
+ free (nc->nodes);
+ memset (nc, 0, sizeof (*nc));
+}