// ******************************************************************************
//
//
//  Version/Date    Comment
//   2026.05.28     - First release
// ******************************************************************************

#include <stdbool.h>
#include <ustdlib.h>

#include "uartstdio.h"

#include "FreeRTOS.h"
#include "task.h"

#include "open62541/server_config_default.h"
#include "open62541/plugin/accesscontrol_default.h"
#include "open62541/server.h"
#include "open62541/plugin/securitypolicy_default.h"
#include "open62541/plugin/certificategroup_default.h"


#include "driverlib/adc.h"
#include "driverlib/gpio.h"
#include "driverlib/sysctl.h"
#include "inc/hw_types.h"
#include "inc/hw_memmap.h"
#include "inc/hw_sysctl.h"

#include "lwip/ip_addr.h"
#include "lwip/netif.h"

#include "TaskOpcUaServer.h"

#ifndef configOPCUASERVER_RX_WINDOW_SIZE
  #define configOPCUASERVER_RX_WINDOW_SIZE    2
#endif

// The port number
#define TASKOPCUASERVER_PORT_NUMBER     4840

// Forwards
static void TaskOpcUaServer_ConnectionTask( void * AParameters );

#ifdef DEBUG
  static void TaskOpcUaServer_Logger(void *context, UA_LogLevel level, UA_LogCategory category, const char *msg, va_list args);

  // The logger definition that points to the logging function
  static UA_Logger TaskOpcUaServer_LoggerDef = {TaskOpcUaServer_Logger, NULL, NULL};
#else
  static UA_Logger TaskOpcUaServer_LoggerDef = {NULL, NULL, NULL};
#endif



// Reference the external byte-array variables generated
extern const unsigned char __project_certificates_server_cert_der[];
extern const unsigned int  __project_certificates_server_cert_der_len;
extern const unsigned char __project_certificates_server_key_der[];
extern const unsigned int  __project_certificates_server_key_der_len;

static const UA_UsernamePasswordLogin TaskOpcUaServer_Logins[1] = {
  {
      UA_STRING_STATIC("admin"),
      UA_STRING_STATIC("admin")
  }
};



// ------------------------------------------------------------------------------
//  Descript: Start the OpcUaServer networking task
//  Params  : <AStackSize>  Stack size
//            <APriority>   Priority
//  Returns : -
// ------------------------------------------------------------------------------
void TaskOpcUaServer_StartOpcUaTasks( size_t AStackSize, UBaseType_t APriority )
{
  configPRINTF( ( "OpcUaServer task init\n" ) );

  if (xTaskCreate(
      TaskOpcUaServer_ConnectionTask,  // The function that implements the task
      "OpcUaServerConnection",         // Just a text name for the task to aid debugging
      AStackSize,                      // The stack size
      NULL,                            // The task parameter, not used in this case
      APriority,                       // The priority assigned to the task
      NULL ) != pdPASS)
  {
    configPRINTF( ( "OpcUaServer task init task failure\n" ) );
    while (1) {}
  }
}


uint32_t TaskOpcUaServer_ReadAdcValue()
{
  uint32_t LAdcValue[1];
  ADCProcessorTrigger(ADC0_BASE, 3);
  while (!ADCIntStatus(ADC0_BASE, 3, false)) { }
  ADCIntClear(ADC0_BASE, 3);
  ADCSequenceDataGet(ADC0_BASE, 3, LAdcValue);
  return LAdcValue[0];
}






#ifdef DEBUG
  // ------------------------------------------------------------------------------
  //  Descript: Get a logging category name
  //  Params  : <ACategory>  Category
  //  Returns : <return>     Name for the provided category
  // ------------------------------------------------------------------------------
  static const char* TaskOpcUaServer_GetCategoryName(UA_LogCategory ACategory) {
    switch (ACategory) {
      case UA_LOGCATEGORY_NETWORK:       return "network  ";
      case UA_LOGCATEGORY_SECURECHANNEL: return "channel  ";
      case UA_LOGCATEGORY_SESSION:       return "session  ";
      case UA_LOGCATEGORY_SERVER:        return "server   ";
      case UA_LOGCATEGORY_CLIENT:        return "client   ";
      case UA_LOGCATEGORY_USERLAND:      return "userland ";
      case UA_LOGCATEGORY_SECURITYPOLICY:return "security ";
      case UA_LOGCATEGORY_EVENTLOOP:     return "eventloop";
      default:                           return "unknown  ";
    }
  }


  // ------------------------------------------------------------------------------
  //  Descript: The logger called by OpcUa
  //            Note that not all is logged correctly
  //  Params  : <ALevel>     Logging level
  //            <ACategory>  Category
  //            <AMsg>       Message
  //            <AArgs>      Message arguments
  //  Returns : -
  // ------------------------------------------------------------------------------
  static void TaskOpcUaServer_Logger(void *context, UA_LogLevel ALevel, UA_LogCategory ACategory, const char *AMsg, va_list AArgs)
  {
    if (ALevel < UA_LOGLEVEL_TRACE) return;

    UARTprintf("[%s] ", TaskOpcUaServer_GetCategoryName(ACategory));
    UARTvprintf(AMsg, AArgs);
    UARTprintf("\n");
  }
#endif


// ------------------------------------------------------------------------------
//  Descript: Called by OpcUa to read the ADC
//  Params  : <AServer>           Server reference
//            <ASessionId>        Session identifier
//            <ASessionContext>   Session context
//            <ANodeId>           Node identifier
//            <ANodeContext>      Node context
//            <ASourceTimestamp>  Timestamp
//            <ARange>            Range
//            <ADataValue>        Data to returtn into
//  Returns : <return>            Success/failure code
//            <ADataValue>        Data from ADC
// ------------------------------------------------------------------------------
static UA_StatusCode TaskOpcUaServer_ReadAdc(UA_Server *AServer, const UA_NodeId *ASessionId, void *ASessionContext,
                                             const UA_NodeId *ANodeId, void *ANodeContext, UA_Boolean ASourceTimeStamp,
                                             const UA_NumericRange *ARange, UA_DataValue *ADataValue)
{
  uint32_t LAdcValue[1];

  LAdcValue[0] = TaskOpcUaServer_ReadAdcValue();

  // Provide the value to the OPC UA stack
  UA_Variant_setScalarCopy(&ADataValue->value, &LAdcValue[0], &UA_TYPES[UA_TYPES_UINT32]);
  ADataValue->hasValue = true;

  return UA_STATUSCODE_GOOD;
}


// ------------------------------------------------------------------------------
//  Descript: Initialize the ADC hardware
//  Params  : -
//  Returns : -
// ------------------------------------------------------------------------------
void TaskOpcUaServer_InitAdc(void)
{
  // Enable Clocks for ADC0 and GPIO Port E (where AIN0/PE3 is located)
  SysCtlPeripheralEnable(SYSCTL_PERIPH_ADC0);
  SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOE);

  // Wait for peripherals to be ready
  while(!SysCtlPeripheralReady(SYSCTL_PERIPH_ADC0));
  while(!SysCtlPeripheralReady(SYSCTL_PERIPH_GPIOE));

  // Configure PE3 as an Analog Input pin
  GPIOPinTypeADC(GPIO_PORTE_BASE, GPIO_PIN_3);

  // Configure ADC Clock (Critical for TM4C1294 @ 120MHz)
  // Use the PIOSC (16MHz) for consistent conversion timing
  ADCClockConfigSet(ADC0_BASE, ADC_CLOCK_SRC_PIOSC | ADC_CLOCK_RATE_FULL, 1);

  // Configure Sample Sequencer 3
  // Use Processor Trigger so we can trigger it manually in the OPC UA callback
  ADCSequenceDisable(ADC0_BASE, 3);
  ADCSequenceConfigure(ADC0_BASE, 3, ADC_TRIGGER_PROCESSOR, 0);

  // Configure the single step in Sequencer 3
  // Read Channel 0 (AIN0), set as end of sequence, and enable interrupt flag
  ADCSequenceStepConfigure(ADC0_BASE, 3, 0, ADC_CTL_CH0 | ADC_CTL_IE | ADC_CTL_END);

  // Enable the sequencer
  ADCSequenceEnable(ADC0_BASE, 3);

  // Clear the interrupt flag to ensure we are ready for the first sample
  ADCIntClear(ADC0_BASE, 3);
}


// ------------------------------------------------------------------------------
//  Descript: Add/setupt the ADC data source the server will provide
//  Params  : <AServer>  Server
//  Returns : -
// ------------------------------------------------------------------------------
void TaskOpcUaServer_AddAdcDataSource(UA_Server *AServer)
{
  // Initialize the ADC hardware
  TaskOpcUaServer_InitAdc();

  UA_DataSource LAdcDataSource;
  LAdcDataSource.read = TaskOpcUaServer_ReadAdc;
  LAdcDataSource.write = NULL; // Set to a function if you want to write to hardware

  UA_VariableAttributes LAttr = UA_VariableAttributes_default;
  LAttr.displayName = UA_LOCALIZEDTEXT("", "TM4C Raw ADC");
  LAttr.description = UA_LOCALIZEDTEXT("", "Analog input");
  LAttr.accessLevel = UA_ACCESSLEVELMASK_READ;

  UA_Server_addDataSourceVariableNode(AServer,
    UA_NODEID_STRING(1, "ADC_Channel_0"),
    UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
    UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
    UA_QUALIFIEDNAME(1, "ADC_Channel_0"),
    UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
    LAttr, LAdcDataSource, NULL, NULL);
}


#include <stdint.h>
#include <stddef.h>
#include "inc/hw_nvic.h"
#include "inc/hw_types.h"


/**
 * @brief Linker-intercepted mock for certificate trust verification.
 *        Forces the open62541 v1.5.4 engine to accept UaExpert's public signature.
 * @return UA_STATUSCODE_GOOD to bypass the trust validation loop.
 */
UA_StatusCode __wrap_UA_CertificateVerification_verifyCertificate(
  const void *cv,
  const UA_ByteString *certificate)
{
  (void)cv; (void)certificate; // Suppress unused parameter warnings
  return UA_STATUSCODE_GOOD;   // Explicitly approve the certificate trust
}

/**
 * @brief Linker-intercepted mock for Application URI matching verification.
 *        Prevents the secure channel from dropping due to URI mismatch rules.
 * @return UA_STATUSCODE_GOOD to accept the connection mapping.
 */
UA_StatusCode __wrap_UA_CertificateVerification_verifyApplicationURI(
  const void *cv,
  const UA_ByteString *certificate,
  const UA_String *applicationURI)
{
  (void)cv; (void)certificate; (void)applicationURI;
  return UA_STATUSCODE_GOOD;   // Explicitly approve the URI alignment
}




void DumpUrls(UA_ServerConfig *LConfig)
{
  char temp_buf[256];
  int copy_len;
  configPRINTF( ("\n--- CONFIGURED SERVER ENDPOINTS (%u found) ---\n", LConfig->endpointsSize ) );

  for(size_t i = 0; i < LConfig->endpointsSize; i++) {
    UA_EndpointDescription *endpoint = &LConfig->endpoints[i];

    // 1. Convert UA_String endpoint URL to a null-terminated format for printf
        // Copy raw bytes and explicitly force null termination
    copy_len = endpoint->endpointUrl.length;
    memcpy(temp_buf, endpoint->endpointUrl.data, copy_len);
    temp_buf[copy_len] = '\0';
    configPRINTF( ("[%u] URL: %s\n", i, temp_buf));

    // 2. Print Security Policy URI
    copy_len = endpoint->securityPolicyUri.length;
    memcpy(temp_buf, endpoint->securityPolicyUri.data, copy_len);
    temp_buf[copy_len] = '\0';
    configPRINTF( ( "    Policy URI: %s\n", temp_buf));

    // 3. Print Message Security Mode
    UARTprintf( "    Security Mode: ");
    switch(endpoint->securityMode) {
        case UA_MESSAGESECURITYMODE_NONE:
            UARTprintf( "None\n");
            break;
        case UA_MESSAGESECURITYMODE_SIGN:
            UARTprintf( "Sign\n");
            break;
        case UA_MESSAGESECURITYMODE_SIGNANDENCRYPT:
            UARTprintf( "Sign & Encrypt\n");
            break;
        default:
            UARTprintf( "Unknown\n");
    }
  }
  configPRINTF( ("\n--- CONFIGURED DISCOVERY URLS (%u found) ---\n", LConfig->applicationDescription.discoveryUrlsSize ) );
  for(size_t i = 0; i < LConfig->applicationDescription.discoveryUrlsSize; i++) {
    copy_len = LConfig->applicationDescription.discoveryUrls[i].length;
    memcpy(temp_buf, LConfig->applicationDescription.discoveryUrls[i].data, copy_len);
    temp_buf[copy_len] = '\0';
    configPRINTF( ("[%u] URL: %s\n", i, temp_buf));
  }
  configPRINTF( ("\n--- CONFIGURED SERVER URLS (%u found) ---\n", LConfig->serverUrlsSize ) );
  for(size_t i = 0; i < LConfig->serverUrlsSize; i++) {
    copy_len = LConfig->serverUrls[i].length;
    memcpy(temp_buf, LConfig->serverUrls[i].data, copy_len);
    temp_buf[copy_len] = '\0';
    configPRINTF( ("[%u] URL: %s\n", i, temp_buf));
  }

  configPRINTF( ("\n--- APPLICATION URI ---\n" ) );
  copy_len = LConfig->applicationDescription.applicationUri.length;
  memcpy(temp_buf, LConfig->applicationDescription.applicationUri.data, copy_len);
  temp_buf[copy_len] = '\0';
  configPRINTF( ("[%u] URI: %s\n", 0, temp_buf));

  configPRINTF( ("\n") );
}




// ------------------------------------------------------------------------------
//  Descript: Start and run the OpcUaServer connection task
//  Params  : <AParameters>  Not used
//  Returns : -
// ------------------------------------------------------------------------------
static void TaskOpcUaServer_ConnectionTask( void * AParameters )
{
  UA_String     LString;
  UA_StatusCode LRetVal;

  configPRINTF( ( "OpcUaServer connection task init\n" ) );
  // Setup the Url string
  UA_String     LUrlString;
  UA_String_format(&LUrlString, "opc.tcp://%s:%d", ipaddr_ntoa(&(netif_default->ip_addr)), TASKOPCUASERVER_PORT_NUMBER);

  UA_Server *LServer = UA_Server_new();
  UA_ServerConfig *LConfig = UA_Server_getConfig(LServer);
  // Set the logger
  LConfig->logging = &TaskOpcUaServer_LoggerDef;
  //
  LRetVal = UA_ServerConfig_setMinimalCustomBuffer(LConfig, TASKOPCUASERVER_PORT_NUMBER, 0, 8192, 8192);
  if (LRetVal != UA_STATUSCODE_GOOD)
    configPRINTF( ("Failure: UA_ServerConfig_setMinimalCustomBuffer\n"));
  //
  // Set the server Urls (the existing one uses a non exiting host name, so replace it)
  if (LConfig->serverUrlsSize > 0)
    UA_Array_delete(LConfig->serverUrls, LConfig->serverUrlsSize, &UA_TYPES[UA_TYPES_STRING]);
  LConfig->serverUrlsSize = 1;
  LConfig->serverUrls = (UA_String *)UA_Array_new(1, &UA_TYPES[UA_TYPES_STRING]);
  if (LConfig->serverUrls)
    UA_String_copy(&LUrlString, &LConfig->serverUrls[0]);
  //
  // Set discovery Urls (we don't expect any being set, but just the same check it)
  // Must be done after UA_ServerConfig_setMinimalCustomBuffer
  if (LConfig->applicationDescription.discoveryUrlsSize > 0)
    UA_Array_delete(LConfig->applicationDescription.discoveryUrls, LConfig->applicationDescription.discoveryUrlsSize, &UA_TYPES[UA_TYPES_STRING]);
  LConfig->applicationDescription.discoveryUrlsSize = 1;
  LConfig->applicationDescription.discoveryUrls = (UA_String *)UA_Array_new(1, &UA_TYPES[UA_TYPES_STRING]);
  if (LConfig->applicationDescription.discoveryUrls)
    UA_String_copy(&LUrlString, &LConfig->applicationDescription.discoveryUrls[0]);
  //
  //
  UA_ByteString LCertificate;
  UA_ByteString LPrivateKey;
  // Populate open62541 structure fields with your real flash locations
  // Explicit typecast to (UA_Byte *) to match open62541 tracking signatures
  LCertificate.length = (size_t)   __project_certificates_server_cert_der_len;
  LCertificate.data   = (UA_Byte *)__project_certificates_server_cert_der;

  LPrivateKey.length  = (size_t)   __project_certificates_server_key_der_len;
  LPrivateKey.data    = (UA_Byte *)__project_certificates_server_key_der;
  //
  // Add the Basic256Sha256 security policy
  if (LRetVal == UA_STATUSCODE_GOOD)
  {
    LRetVal = UA_ServerConfig_addSecurityPolicyBasic256Sha256(LConfig, &LCertificate, &LPrivateKey);
    if (LRetVal != UA_STATUSCODE_GOOD)
      configPRINTF( ("Failure: UA_ServerConfig_addSecurityPolicyBasic256Sha256\n"));
  }
  if (LRetVal == UA_STATUSCODE_GOOD)
  {
    LRetVal = UA_AccessControl_default(LConfig, false, NULL, 0, NULL);
    if (LRetVal != UA_STATUSCODE_GOOD)
      configPRINTF( ("Failure: UA_AccessControl_default\n"));
  }
  if (LRetVal == UA_STATUSCODE_GOOD)
  {
    LRetVal = UA_ServerConfig_addAllSecureEndpoints(LConfig);
    if (LRetVal != UA_STATUSCODE_GOOD)
      configPRINTF( ("Failure: UA_ServerConfig_addAllSecureEndpoints\n"));
  }
  if (LRetVal != UA_STATUSCODE_GOOD)
  {
    UA_ServerConfig_clear(LConfig);
    configPRINTF( ("Failure: Stopping further processing\n"));
    while(1) { } ;
  }
  // Adds the None policy to the security policy list, but does not provide a None endpoint
  // This enables a client to retrieve the server certificate and all endpoints offered by a server
  UA_ServerConfig_addSecurityPolicyNone(LConfig, &LCertificate);
  LConfig->securityPolicyNoneDiscoveryOnly = true;
  // Accept all certificates
  LConfig->secureChannelPKI.clear(&LConfig->secureChannelPKI);
  UA_CertificateGroup_AcceptAll  (&LConfig->secureChannelPKI);
  //
  LConfig->sessionPKI.clear    (&LConfig->sessionPKI);
  UA_CertificateGroup_AcceptAll(&LConfig->sessionPKI);

  // Add username/password auth
  LConfig->accessControl.clear(&LConfig->accessControl);
  const UA_String LUserTokenPolicy = UA_STRING("http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256");
  UA_AccessControl_default(LConfig, false, &LUserTokenPolicy, 1, TaskOpcUaServer_Logins);

  // Clean up
  LCertificate.length = 0;
  LCertificate.data   = NULL;
  LPrivateKey.length  = 0;
  LPrivateKey.data    = NULL;

  // We only want the 'sign and encrypt' endpoint
  // if (LConfig->endpointsSize >= 2) {
  //   UA_EndpointDescription *LNewEndpoints = (UA_EndpointDescription *)UA_malloc(sizeof(UA_EndpointDescription) * 1);
  //   // Keep Sign & Encrypt endpoint
  //   LNewEndpoints[0] = LConfig->endpoints[1];
  //   // Safely free the original larger array container (does not clear underlying endpoints)
  //   UA_free(LConfig->endpoints);
  //   // Update configuration pointers and size counters
  //   LConfig->endpoints = LNewEndpoints;
  //   LConfig->endpointsSize = 1;
  // }


  // Set endpoint Urls
  for (size_t i = 0; i < LConfig->endpointsSize; i++)
  {
    // Clear out the blank/invalid string memory initially generated
    UA_String_clear(&LConfig->endpoints[i].endpointUrl);
    // Set the endpoint name
    UA_String_copy(&LUrlString, &LConfig->endpoints[i].endpointUrl);
  }

  //
  // This MUST match that was used when generating the certificate!
  // It must be set before UA_ServerConfig_setMinimalCustomBuffer is called
                  LString = UA_STRING("urn:open62541:server:tiva");
  UA_String_copy(&LString, &LConfig->applicationDescription.applicationUri);

  //
  // Since we are not using a preset namespace ....
  // We need to create some basic namespace nodes
  // The initNS0 should take care of that
  extern UA_StatusCode initNS0(UA_Server *server);
  initNS0(LServer);
  // We must make sure that the variable node is not marked as 'abstract'
  // Directly write the attribute value to the server
  UA_Server_writeIsAbstract(LServer, UA_NODEID_NUMERIC(0, 62), false);
  //
  // Now change some of out information we provide
  // We do this after initNS0() since that changes things
  LConfig = UA_Server_getConfig(LServer);
  // Set the logger
  LConfig->logging = &TaskOpcUaServer_LoggerDef;
  //
  // DisplayName, use the specialized write function (Server node)
  UA_Server_writeDisplayName(LServer, UA_NODEID_NUMERIC(0, 2253), UA_LOCALIZEDTEXT("", "Server DisplayName"));
  // Description, use the specialized write function (Server node)
  UA_Server_writeDescription(LServer, UA_NODEID_NUMERIC(0, 2253), UA_LOCALIZEDTEXT("", "Server Description"));
  // DisplayName, use the specialized write function (ServerArray node)
  UA_Server_writeDisplayName(LServer, UA_NODEID_NUMERIC(0, 2254), UA_LOCALIZEDTEXT("", "ServerArray DisplayName"));
  // Description, use the specialized write function (ServerArray node)
  UA_Server_writeDescription(LServer, UA_NODEID_NUMERIC(0, 2254), UA_LOCALIZEDTEXT("", "ServerArray Description"));
  // DisplayName, use the specialized write function (NamespaceArray node)
  UA_Server_writeDisplayName(LServer, UA_NODEID_NUMERIC(0, 2255), UA_LOCALIZEDTEXT("", "NamespaceArray DisplayName"));
  // Description, use the specialized write function (NamespaceArray node)
  UA_Server_writeDescription(LServer, UA_NODEID_NUMERIC(0, 2255), UA_LOCALIZEDTEXT("", "NamespaceArray Description"));
  // DisplayName, use the specialized write function (ServerStatus node)
  UA_Server_writeDisplayName(LServer, UA_NODEID_NUMERIC(0, 2256), UA_LOCALIZEDTEXT("", "ServerStatus DisplayName"));
  // Description, use the specialized write function (ServerStatus node)
  UA_Server_writeDescription(LServer, UA_NODEID_NUMERIC(0, 2256), UA_LOCALIZEDTEXT("", "ServerStatus Description"));
  // DisplayName, use the specialized write function (BuildInfo node)
  UA_Server_writeDisplayName(LServer, UA_NODEID_NUMERIC(0, 2260), UA_LOCALIZEDTEXT("", "BuildInfo DisplayName"));
  // Description, use the specialized write function (BuildInfo node)
  UA_Server_writeDescription(LServer, UA_NODEID_NUMERIC(0, 2260), UA_LOCALIZEDTEXT("", "BuildInfo Description"));
  // Etcetera
  //
  // Set Application URI (Unique instance ID)
  // This MUST match that was used when generating the certificate!
                  LString = UA_STRING("urn:open62541:server:tiva");
  UA_String_copy(&LString, &LConfig->applicationDescription.applicationUri);
  // Set build information
                  LString = UA_STRING("Our productUri");
  UA_String_copy(&LString, &LConfig->buildInfo.productUri);
                  LString = UA_STRING("Our manufacturerName");
  UA_String_copy(&LString, &LConfig->buildInfo.manufacturerName);
                  LString = UA_STRING("Our productName");
  UA_String_copy(&LString, &LConfig->buildInfo.productName);
                  LString = UA_STRING("Our softwareVersion");
  UA_String_copy(&LString, &LConfig->buildInfo.softwareVersion);
                  LString = UA_STRING("Our buildNumber");
  UA_String_copy(&LString, &LConfig->buildInfo.buildNumber);
  // And finally a date/time thing
  UA_DateTimeStruct LDateTimeStruct;
  memset(&LDateTimeStruct, 0, sizeof(UA_DateTimeStruct));
  // Set your desired parameters
  LDateTimeStruct.year  = 2026;
  LDateTimeStruct.month = 5;  // 1 to 12
  LDateTimeStruct.day   = 10; // 1 to 31
  LDateTimeStruct.hour  = 12;
  LDateTimeStruct.min   = 30;
  LDateTimeStruct.sec   = 0;
  // Convert to UA_DateTime (the 64-bit integer format)
  LConfig->buildInfo.buildDate = UA_DateTime_fromStruct(LDateTimeStruct);

  //
  // Just out of curiosity we try to be as fast as possible ....
  // A simple performance test show that an avarage (ADC) read of something less
  // than 2 ms is possible (changing the main loop delay/settings does not make too much
  // of a difference)
  // Try getting a faster sampling interval than the default 50 ms
  // Set the sampling interval limits (Min, Max)
  // How often data is sampled
  LConfig->samplingIntervalLimits.min   = 1.0;
  // Also adjust the publishing interval (how often data is communicated)
  LConfig->publishingIntervalLimits.min = 1.0;

  //
  // Add ADC to our what the server can 'provide'
  TaskOpcUaServer_AddAdcDataSource(LServer);


DumpUrls(LConfig);


  //
  // Now start the server and process data
  UA_Server_run_startup(LServer);

  while (1) {
    // UA_Server_run_iterate does all the heavy lifting:
    // . Check for incoming network packets (lwIP)
    // . If a client asks for the ADC, it triggers the TaskOpcUaServer_ReadAdc() function
    // . Handles internal timers (subscriptions, timeouts)
    // The 'true' parameter tells it to process all expired timers
    UA_UInt16 LWaitTime = UA_Server_run_iterate(LServer, true);
    // Use suggested wait value
    vTaskDelay(pdMS_TO_TICKS(LWaitTime));

    // The 'fastest' loop (using vTaskDelayUntil instead might help)
    // UA_Server_run_iterate(LServer, false);
    // vTaskDelay(1);
  }
  // We will never get here ....
  UA_Server_run_shutdown(LServer);
  vTaskDelete(NULL);
}
