aboutsummaryrefslogtreecommitdiffstats
path: root/src/client_find_servers.c
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-02-17 02:27:51 +0100
committerThomas Vanbesien <tvanbesi@proton.me>2026-02-17 02:27:51 +0100
commitc35eb35bb63a97b7c46e879819757a9cb48165b5 (patch)
treeabc7f07740fae388f4ff6776585b56f56ec558c9 /src/client_find_servers.c
downloadBobinkCOpcUa-c35eb35bb63a97b7c46e879819757a9cb48165b5.tar.gz
BobinkCOpcUa-c35eb35bb63a97b7c46e879819757a9cb48165b5.zip
Initial commit: OPC UA discovery project
CMake-based C project using open62541 for OPC UA discovery. Includes Local Discovery Server, register server, and find servers client with OpenSSL encryption support.
Diffstat (limited to 'src/client_find_servers.c')
-rw-r--r--src/client_find_servers.c281
1 files changed, 281 insertions, 0 deletions
diff --git a/src/client_find_servers.c b/src/client_find_servers.c
new file mode 100644
index 0000000..c62fc15
--- /dev/null
+++ b/src/client_find_servers.c
@@ -0,0 +1,281 @@
+/**
+ * @file client_find_servers.c
+ * @brief OPC UA client that queries a Local Discovery Server for registered
+ * servers.
+ *
+ * This program connects to an LDS and calls the FindServers service to
+ * retrieve all registered servers. It then queries each server's endpoints
+ * using the GetEndpoints service and displays the results in a human-readable
+ * format.
+ */
+
+#include "common.h"
+
+#include <open62541/client_highlevel.h>
+#include <open62541/plugin/log_stdout.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+/* ========================================================================
+ * Discovery Service Calls
+ * ======================================================================== */
+
+/**
+ * Calls the FindServers service on the LDS and prints all discovered servers.
+ *
+ * @param client The OPC UA client instance.
+ * @param discoveryServerEndpoint The LDS endpoint URL.
+ * @param applicationDescriptionArraySize Output: number of servers found.
+ * @param applicationDescriptionArray Output: array of server descriptions.
+ * @return UA_STATUSCODE_GOOD on success, error code otherwise.
+ */
+static UA_StatusCode
+findServers (UA_Client *client, const char *discoveryServerEndpoint,
+ size_t *applicationDescriptionArraySize,
+ UA_ApplicationDescription **applicationDescriptionArray)
+{
+ UA_StatusCode retval = UA_Client_findServers (
+ client, discoveryServerEndpoint, 0, NULL, 0, NULL,
+ applicationDescriptionArraySize, applicationDescriptionArray);
+
+ if (retval != UA_STATUSCODE_GOOD)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_SERVER,
+ "Could not call FindServers service. "
+ "Is the discovery server started? StatusCode %s",
+ UA_StatusCode_name (retval));
+ return retval;
+ }
+
+ for (size_t i = 0; i < *applicationDescriptionArraySize; i++)
+ printApplicationDescription (&(*applicationDescriptionArray)[i], i);
+
+ return UA_STATUSCODE_GOOD;
+}
+
+/**
+ * Queries endpoints for each discovered server using the GetEndpoints service.
+ *
+ * For each server in the applicationDescriptionArray, this function extracts
+ * the first discovery URL and calls GetEndpoints to retrieve all available
+ * endpoints. Results are logged via UA_LOG_INFO.
+ *
+ * @param client The OPC UA client instance.
+ * @param applicationDescriptionArray Array of server descriptions from
+ * FindServers.
+ * @param applicationDescriptionArraySize Number of servers in the array.
+ */
+static void
+getServersEndpoints (UA_Client *client,
+ UA_ApplicationDescription *applicationDescriptionArray,
+ size_t applicationDescriptionArraySize)
+{
+ for (size_t i = 0; i < applicationDescriptionArraySize; i++)
+ {
+ UA_ApplicationDescription *description = &applicationDescriptionArray[i];
+ if (description->discoveryUrlsSize == 0)
+ {
+ UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
+ "[GetEndpoints] Server %.*s did not provide any "
+ "discovery urls. Skipping.",
+ (int)description->applicationUri.length,
+ description->applicationUri.data);
+ continue;
+ }
+
+ /* UA_String is not null-terminated; build a C string for the API. */
+ char *discoveryUrl = (char *)UA_malloc (
+ sizeof (char) * description->discoveryUrls[0].length + 1);
+ memcpy (discoveryUrl, description->discoveryUrls[0].data,
+ description->discoveryUrls[0].length);
+ discoveryUrl[description->discoveryUrls[0].length] = '\0';
+
+ UA_EndpointDescription *endpointArray = NULL;
+ size_t endpointArraySize = 0;
+ UA_StatusCode retval = UA_Client_getEndpoints (
+ client, discoveryUrl, &endpointArraySize, &endpointArray);
+ UA_free (discoveryUrl);
+ if (retval != UA_STATUSCODE_GOOD)
+ {
+ UA_Client_disconnect (client);
+ break;
+ }
+
+ UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_APPLICATION,
+ "Endpoints for Server[%lu]: %.*s", (unsigned long)i,
+ (int)description->applicationUri.length,
+ description->applicationUri.data);
+ for (size_t j = 0; j < endpointArraySize; j++)
+ printEndpoint (&endpointArray[j], j);
+
+ UA_Array_delete (endpointArray, endpointArraySize,
+ &UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]);
+ }
+}
+
+/* ========================================================================
+ * Node Reading
+ * ======================================================================== */
+
+/**
+ * Connects to each non-discovery server and reads the current time node.
+ *
+ * For each server that is not a DiscoveryServer, this function establishes a
+ * secure session, reads the Server_ServerStatus_CurrentTime variable, prints
+ * the result, and disconnects.
+ *
+ * @param client The OPC UA client instance.
+ * @param applicationDescriptionArray Array of server descriptions from
+ * FindServers.
+ * @param applicationDescriptionArraySize Number of servers in the array.
+ */
+static void
+readServerTime (UA_Client *client,
+ UA_ApplicationDescription *applicationDescriptionArray,
+ size_t applicationDescriptionArraySize)
+{
+ for (size_t i = 0; i < applicationDescriptionArraySize; i++)
+ {
+ UA_ApplicationDescription *desc = &applicationDescriptionArray[i];
+
+ if (desc->applicationType == UA_APPLICATIONTYPE_DISCOVERYSERVER)
+ continue;
+
+ if (desc->discoveryUrlsSize == 0)
+ {
+ UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
+ "[ReadTime] Server %.*s has no discovery URLs. "
+ "Skipping.",
+ (int)desc->applicationUri.length,
+ desc->applicationUri.data);
+ continue;
+ }
+
+ /* UA_String is not null-terminated; build a C string for the API. */
+ char *url = (char *)UA_malloc (desc->discoveryUrls[0].length + 1);
+ memcpy (url, desc->discoveryUrls[0].data, desc->discoveryUrls[0].length);
+ url[desc->discoveryUrls[0].length] = '\0';
+
+ UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
+ "Connecting to %s to read current time...", url);
+
+ UA_StatusCode retval = UA_Client_connect (client, url);
+ UA_free (url);
+ if (retval != UA_STATUSCODE_GOOD)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
+ "Could not connect: %s", UA_StatusCode_name (retval));
+ continue;
+ }
+
+ UA_Variant value;
+ UA_Variant_init (&value);
+
+ const UA_NodeId nodeId = UA_NS0ID (SERVER_SERVERSTATUS_CURRENTTIME);
+ retval = UA_Client_readValueAttribute (client, nodeId, &value);
+
+ if (retval == UA_STATUSCODE_GOOD
+ && UA_Variant_hasScalarType (&value, &UA_TYPES[UA_TYPES_DATETIME]))
+ {
+ UA_DateTime raw_date = *(UA_DateTime *)value.data;
+ UA_DateTimeStruct dts = UA_DateTime_toStruct (raw_date);
+ UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_APPLICATION,
+ "date is: %u-%u-%u %u:%u:%u.%03u", dts.day, dts.month,
+ dts.year, dts.hour, dts.min, dts.sec, dts.milliSec);
+ }
+ else
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
+ "Could not read current time: %s",
+ UA_StatusCode_name (retval));
+ }
+
+ UA_Variant_clear (&value);
+ UA_Client_disconnect (client);
+ }
+}
+
+/* ========================================================================
+ * Main
+ * ======================================================================== */
+
+int
+main (int argc, char **argv)
+{
+ if (argc < 7)
+ {
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Usage: %s <discovery-server-endpoint>\n"
+ " <applicationUri>\n"
+ " <certificate.der> <private-key.der>\n"
+ " <security-mode> <security-policy>\n"
+ " [<trustlist1.der>, ...]\n"
+ "\n"
+ "Security modes : None, Sign, SignAndEncrypt\n"
+ "Security policies: None, Basic256Sha256, "
+ "Aes256_Sha256_RsaPss,\n"
+ " Aes128_Sha256_RsaOaep, ECC_nistP256",
+ argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ const char *discoveryServerEndpoint = argv[1];
+ const char *applicationUri = argv[2];
+ const char *certPath = argv[3];
+ const char *keyPath = argv[4];
+
+ UA_MessageSecurityMode securityMode = parseSecurityMode (argv[5]);
+ if (securityMode == UA_MESSAGESECURITYMODE_INVALID)
+ {
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Unknown security mode: %s", argv[5]);
+ return EXIT_FAILURE;
+ }
+
+ const char *securityPolicyUri = resolveSecurityPolicyUri (argv[6]);
+ if (!securityPolicyUri)
+ {
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Unknown security policy: %s", argv[6]);
+ return EXIT_FAILURE;
+ }
+
+ char **trustPaths = argv + 7;
+ size_t trustSize = (argc > 7) ? (size_t)argc - 7 : 0;
+
+ UA_Client *client = UA_Client_new ();
+ UA_StatusCode retval = createSecureClientConfig (
+ UA_Client_getConfig (client), applicationUri, certPath, keyPath,
+ trustPaths, trustSize, securityMode, securityPolicyUri);
+ if (retval != UA_STATUSCODE_GOOD)
+ {
+ UA_Client_delete (client);
+ return EXIT_FAILURE;
+ }
+
+ UA_ApplicationDescription *applicationDescriptionArray = NULL;
+ size_t applicationDescriptionArraySize = 0;
+
+ retval = findServers (client, discoveryServerEndpoint,
+ &applicationDescriptionArraySize,
+ &applicationDescriptionArray);
+ if (retval != UA_STATUSCODE_GOOD)
+ {
+ UA_Client_delete (client);
+ return EXIT_FAILURE;
+ }
+
+ getServersEndpoints (client, applicationDescriptionArray,
+ applicationDescriptionArraySize);
+
+ readServerTime (client, applicationDescriptionArray,
+ applicationDescriptionArraySize);
+
+ UA_Client_delete (client);
+ UA_Array_delete (applicationDescriptionArray,
+ applicationDescriptionArraySize,
+ &UA_TYPES[UA_TYPES_APPLICATIONDESCRIPTION]);
+
+ return EXIT_SUCCESS;
+}