diff options
| author | Thomas Vanbesien <tvanbesi@proton.me> | 2026-02-17 02:27:51 +0100 |
|---|---|---|
| committer | Thomas Vanbesien <tvanbesi@proton.me> | 2026-02-17 02:27:51 +0100 |
| commit | c35eb35bb63a97b7c46e879819757a9cb48165b5 (patch) | |
| tree | abc7f07740fae388f4ff6776585b56f56ec558c9 /src/client_find_servers.c | |
| download | BobinkCOpcUa-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.c | 281 |
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; +} |
