diff options
| author | Thomas Vanbesien <tvanbesi@proton.me> | 2026-02-19 06:19:23 +0100 |
|---|---|---|
| committer | Thomas Vanbesien <tvanbesi@proton.me> | 2026-02-19 06:19:23 +0100 |
| commit | 2b632bd229edaa9999be5043f9a8ae2ac7d17e41 (patch) | |
| tree | 5071ef9fa36a898fbe009f477441fd2f34a4cb2d /src | |
| parent | 37c0fee672afd3701ea3ed87958da4d548bf1be3 (diff) | |
| download | BobinkCOpcUa-2b632bd229edaa9999be5043f9a8ae2ac7d17e41.tar.gz BobinkCOpcUa-2b632bd229edaa9999be5043f9a8ae2ac7d17e41.zip | |
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')
| -rw-r--r-- | src/nodes_config.c | 738 | ||||
| -rw-r--r-- | src/nodes_config.h | 126 | ||||
| -rw-r--r-- | src/server_register.c | 39 |
3 files changed, 900 insertions, 3 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)); +} diff --git a/src/nodes_config.h b/src/nodes_config.h new file mode 100644 index 0000000..0207a55 --- /dev/null +++ b/src/nodes_config.h @@ -0,0 +1,126 @@ +#ifndef DISCOVERY_NODES_CONFIG_H +#define DISCOVERY_NODES_CONFIG_H + +/** + * @file nodes_config.h + * @brief Dot-indexed node configuration parser and server-side node creator. + * + * Reads a config file containing node.N.field entries, parses them into + * an array of node descriptors, and creates corresponding OPC UA variable + * nodes in the server address space. Each node gets a string NodeId in + * namespace 1 (ns=1;s=<name>). + * + * Supported types (scalar and 1D array via "type[]" suffix): + * bool, int16, uint16, int32, uint32, int64, uint64, float, double, string + * + * Access levels: read, readwrite. + */ + +#include <open62541/server.h> + +#include <stddef.h> + +/* ======================================================================== + * Types + * ======================================================================== */ + +/** + * @brief OPC UA data types supported by the nodes config parser. + */ +typedef enum +{ + NODE_TYPE_BOOL, + NODE_TYPE_INT16, + NODE_TYPE_UINT16, + NODE_TYPE_INT32, + NODE_TYPE_UINT32, + NODE_TYPE_INT64, + NODE_TYPE_UINT64, + NODE_TYPE_FLOAT, + NODE_TYPE_DOUBLE, + NODE_TYPE_STRING +} node_type; + +/** + * @brief Access level flags for a variable node. + */ +typedef enum +{ + NODE_ACCESS_READ, + NODE_ACCESS_READWRITE +} node_access_level; + +/** + * @brief Describes a single variable node to be created on the server. + * + * All string pointers are heap-allocated copies owned by this struct. + * Call nodes_config_free() to release them. + */ +typedef struct +{ + char *name; /**< DisplayName, browseName, and string NodeId. */ + char *description; /**< Optional description text (NULL when absent). */ + node_type type; /**< OPC UA data type. */ + UA_Boolean is_array; /**< true when the type name ended with "[]". */ + char *value_str; /**< Raw value string (comma-separated for arrays). */ + node_access_level access; /**< Read or read-write. */ +} node_config; + +/** + * @brief A parsed set of variable node definitions. + */ +typedef struct +{ + node_config *nodes; + size_t count; +} nodes_config; + +/* ======================================================================== + * Public API + * ======================================================================== */ + +/** + * @brief Parses a nodes configuration file. + * + * Reads dot-indexed keys (node.N.name, node.N.type, etc.) from the + * file at @p path, groups them by index, validates required fields, + * and populates a nodes_config struct. The caller must free the + * result with nodes_config_free(). + * + * When the file contains no node.* keys the function succeeds with + * out->count == 0 and out->nodes == NULL. + * + * @param path Path to the nodes configuration file. + * @param out Output: parsed nodes configuration. + * @return 0 on success, -1 on error (logged via UA_LOG_ERROR/FATAL). + */ +int nodes_config_load (const char *path, nodes_config *out); + +/** + * @brief Adds all configured variable nodes to a server. + * + * For each node_config entry, parses the value string into the + * appropriate UA type, constructs a UA_VariableAttributes, and calls + * UA_Server_addVariableNode with a string NodeId in namespace 1 + * under the Objects folder. + * + * Individual node failures are logged but do not stop processing of + * remaining nodes. + * + * @param server The UA_Server to add nodes to. + * @param nc The parsed nodes configuration. + * @return 0 when all nodes were added, -1 if any node failed. + */ +int nodes_config_add (UA_Server *server, const nodes_config *nc); + +/** + * @brief Frees all memory owned by a nodes_config structure. + * + * After this call the structure is zeroed and must not be used + * unless nodes_config_load() is called again. + * + * @param nc The nodes configuration to free. + */ +void nodes_config_free (nodes_config *nc); + +#endif /* DISCOVERY_NODES_CONFIG_H */ diff --git a/src/server_register.c b/src/server_register.c index b1d87fd..b938ca4 100644 --- a/src/server_register.c +++ b/src/server_register.c @@ -10,6 +10,7 @@ #include "common.h" #include "config.h" +#include "nodes_config.h" #include <open62541/client.h> #include <open62541/client_config_default.h> @@ -110,18 +111,36 @@ main (int argc, char **argv) signal (SIGINT, _s_stop_handler); signal (SIGTERM, _s_stop_handler); - if (argc < 4 || argc > 5) + if (argc < 4 || argc > 6) { UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Usage: %s <server-config> <client-config> " - "<discovery-url> [log-level]", + "<discovery-url> [nodes-config] [log-level]", argv[0]); return EXIT_FAILURE; } const char *discovery_endpoint = argv[3]; - const char *log_level_str = (argc == 5) ? argv[4] : "info"; + /* Parse the optional [nodes-config] and [log-level] arguments. + When only one optional arg is given, disambiguate by checking + whether it is a valid log-level name. */ + const char *nodes_config_path = NULL; + const char *log_level_str = "info"; + + if (argc == 6) + { + nodes_config_path = argv[4]; + log_level_str = argv[5]; + } + else if (argc == 5) + { + if (parse_log_level (argv[4]) >= 0) + log_level_str = argv[4]; + else + nodes_config_path = argv[4]; + } + int log_level = parse_log_level (log_level_str); if (log_level < 0) { @@ -139,6 +158,7 @@ main (int argc, char **argv) config client_cfg = { 0 }; security_config server_sec = { 0 }; security_config client_sec = { 0 }; + nodes_config nodes_cfg = { 0 }; UA_Server *server = NULL; if (config_load (argv[1], &server_cfg) != 0) @@ -198,6 +218,18 @@ main (int argc, char **argv) server_config->applicationDescription.applicationType = UA_APPLICATIONTYPE_SERVER; + /* ── Load and create variable nodes (optional) ───────────── */ + + if (nodes_config_path) + { + if (nodes_config_load (nodes_config_path, &nodes_cfg) != 0) + goto cleanup; + + if (nodes_cfg.count > 0 && nodes_config_add (server, &nodes_cfg) != 0) + UA_LOG_WARNING (UA_Log_Stdout, UA_LOGCATEGORY_SERVER, + "Some nodes failed to initialize"); + } + lds_client_params lds_params = { .app_uri = client_app_uri, .sec = client_sec, @@ -250,6 +282,7 @@ main (int argc, char **argv) cleanup: if (server) UA_Server_delete (server); + nodes_config_free (&nodes_cfg); free_trust_store (client_sec.trust_paths, client_sec.trust_size); free_trust_store (server_sec.trust_paths, server_sec.trust_size); config_free (&client_cfg); |
