Water Pump Simulation Server

 // Copyright (C) 2018 basysKom GmbH, opensource@basyskom.com
 // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

 #include "simulationserver.h"

 #include <qopen62541utils.h>
 #include <qopen62541valueconverter.h>

 #include <QtCore/QDebug>
 #include <QtCore/QLoggingCategory>

 #include <cstring>

 using namespace Qt::Literals::StringLiterals;

 // Node ID conversion is included from the open62541 plugin but warnings from there should be logged
 // using qt.opcua.testserver instead of qt.opcua.plugins.open62541 for usage in the test server
 Q_LOGGING_CATEGORY(QT_OPCUA_PLUGINS_OPEN62541, "qt.opcua.demoserver")

 DemoServer::DemoServer(QObject *parent)
     : QObject(parent)
     , m_state(DemoServer::MachineState::Idle)
     , m_percentFilledTank1(100)
     , m_percentFilledTank2(0)
 {
     m_timer.setInterval(0);
     m_timer.setSingleShot(false);
     m_machineTimer.setInterval(200);
     connect(&m_timer, &QTimer::timeout, this, &DemoServer::processServerEvents);
 }

 DemoServer::~DemoServer()
 {
     shutdown();
     UA_Server_delete(m_server);
     UA_NodeId_clear(&m_percentFilledTank1Node);
     UA_NodeId_clear(&m_percentFilledTank2Node);
     UA_NodeId_clear(&m_tank2TargetPercentNode);
     UA_NodeId_clear(&m_tank2ValveStateNode);
     UA_NodeId_clear(&m_machineStateNode);
 }

 bool DemoServer::init()
 {
     m_server = UA_Server_new();

     if (!m_server)
         return false;

     UA_StatusCode result = UA_ServerConfig_setMinimal(UA_Server_getConfig(m_server), 43344, nullptr);

     if (result != UA_STATUSCODE_GOOD)
         return false;

     return true;
 }

 void DemoServer::processServerEvents()
 {
     if (m_running)
         UA_Server_run_iterate(m_server, true);
 }

 void DemoServer::shutdown()
 {
     if (m_running) {
         UA_Server_run_shutdown(m_server);
         m_running = false;
     }
 }

 UA_NodeId DemoServer::addObject(const QString &parent, const QString &nodeString,
                                 const QString &browseName, const QString &displayName,
                                 const QString &description, quint32 referenceType)
 {
     UA_NodeId resultNode;
     UA_ObjectAttributes oAttr = UA_ObjectAttributes_default;

     oAttr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", displayName.toUtf8().constData());
     if (description.size())
         oAttr.description = UA_LOCALIZEDTEXT_ALLOC("en-US", description.toUtf8().constData());

     UA_StatusCode result;
     UA_NodeId requestedNodeId = Open62541Utils::nodeIdFromQString(nodeString);
     UA_NodeId parentNodeId = Open62541Utils::nodeIdFromQString(parent);

     UA_QualifiedName nodeBrowseName = UA_QUALIFIEDNAME_ALLOC(requestedNodeId.namespaceIndex,
                                                              browseName.toUtf8().constData());

     result = UA_Server_addObjectNode(m_server,
                                      requestedNodeId,
                                      parentNodeId,
                                      UA_NODEID_NUMERIC(0, referenceType),
                                      nodeBrowseName,
                                      UA_NODEID_NULL,
                                      oAttr,
                                      nullptr,
                                      &resultNode);

     UA_QualifiedName_clear(&nodeBrowseName);
     UA_NodeId_clear(&requestedNodeId);
     UA_NodeId_clear(&parentNodeId);
     UA_ObjectAttributes_clear(&oAttr);

     if (result != UA_STATUSCODE_GOOD) {
         qWarning() << "Could not add folder:" << nodeString << " :" << result;
         return UA_NODEID_NULL;
     }
     return resultNode;
 }

 UA_NodeId DemoServer::addVariable(const UA_NodeId &folder, const QString &variableNode,
                                   const QString &browseName, const QString &displayName,
                                   const QVariant &value, QOpcUa::Types type, quint32 referenceType)
 {
     UA_NodeId variableNodeId = Open62541Utils::nodeIdFromQString(variableNode);

     UA_VariableAttributes attr = UA_VariableAttributes_default;
     attr.value = QOpen62541ValueConverter::toOpen62541Variant(value, type);
     attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", displayName.toUtf8().constData());
     attr.dataType = attr.value.type ? attr.value.type->typeId : UA_TYPES[UA_TYPES_BOOLEAN].typeId;
     attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;

     UA_QualifiedName variableName = UA_QUALIFIEDNAME_ALLOC(variableNodeId.namespaceIndex,
                                                            browseName.toUtf8().constData());

     UA_NodeId resultId;
     UA_StatusCode result = UA_Server_addVariableNode(m_server,
                                                      variableNodeId,
                                                      folder,
                                                      UA_NODEID_NUMERIC(0, referenceType),
                                                      variableName,
                                                      UA_NODEID_NULL,
                                                      attr,
                                                      nullptr,
                                                      &resultId);

     UA_NodeId_clear(&variableNodeId);
     UA_VariableAttributes_clear(&attr);
     UA_QualifiedName_clear(&variableName);

     if (result != UA_STATUSCODE_GOOD) {
         qWarning() << "Could not add variable:" << result;
         return UA_NODEID_NULL;
     }

     return resultId;
 }

 UA_StatusCode DemoServer::startPumpMethod(UA_Server *server,
                                           const UA_NodeId *sessionId, void *sessionHandle,
                                           const UA_NodeId *methodId, void *methodContext,
                                           const UA_NodeId *objectId, void *objectContext,
                                           size_t inputSize, const UA_Variant *input,
                                           size_t outputSize, UA_Variant *output)
 {
     Q_UNUSED(server);
     Q_UNUSED(sessionId);
     Q_UNUSED(sessionHandle);
     Q_UNUSED(methodId);
     Q_UNUSED(objectId);
     Q_UNUSED(objectContext);
     Q_UNUSED(inputSize);
     Q_UNUSED(input);
     Q_UNUSED(outputSize);
     Q_UNUSED(output);

     DemoServer *data = static_cast<DemoServer *>(methodContext);

     double targetValue = data->readTank2TargetValue();

     if (data->m_state == MachineState::Idle
         && data->m_percentFilledTank1 > 0
         && data->m_percentFilledTank2 < targetValue) {
         qDebug() << "Start pumping";
         data->setState(MachineState::Pumping);
         data->m_machineTimer.start();
         return UA_STATUSCODE_GOOD;
     }
     else {
         qDebug() << "Machine already running";
         return UA_STATUSCODE_BADUSERACCESSDENIED;
     }
 }

 UA_StatusCode DemoServer::stopPumpMethod(UA_Server *server,
                                          const UA_NodeId *sessionId, void *sessionHandle,
                                          const UA_NodeId *methodId, void *methodContext,
                                          const UA_NodeId *objectId, void *objectContext,
                                          size_t inputSize, const UA_Variant *input,
                                          size_t outputSize, UA_Variant *output)
 {
     Q_UNUSED(server);
     Q_UNUSED(sessionId);
     Q_UNUSED(sessionHandle);
     Q_UNUSED(methodId);
     Q_UNUSED(objectId);
     Q_UNUSED(objectContext);
     Q_UNUSED(inputSize);
     Q_UNUSED(input);
     Q_UNUSED(outputSize);
     Q_UNUSED(output);

     DemoServer *data = static_cast<DemoServer *>(methodContext);

     if (data->m_state == MachineState::Pumping) {
         qDebug() << "Stopping";
         data->m_machineTimer.stop();
         data->setState(MachineState::Idle);
         return UA_STATUSCODE_GOOD;
     } else {
         qDebug() << "Nothing to stop";
         return UA_STATUSCODE_BADUSERACCESSDENIED;
     }
 }

 UA_StatusCode DemoServer::flushTank2Method(UA_Server *server,
                                            const UA_NodeId *sessionId, void *sessionHandle,
                                            const UA_NodeId *methodId, void *methodContext,
                                            const UA_NodeId *objectId, void *objectContext,
                                            size_t inputSize, const UA_Variant *input,
                                            size_t outputSize, UA_Variant *output)
 {
     Q_UNUSED(server);
     Q_UNUSED(sessionId);
     Q_UNUSED(sessionHandle);
     Q_UNUSED(methodId);
     Q_UNUSED(objectId);
     Q_UNUSED(objectContext);
     Q_UNUSED(inputSize);
     Q_UNUSED(input);
     Q_UNUSED(outputSize);
     Q_UNUSED(output);

     DemoServer *data = static_cast<DemoServer *>(methodContext);

     double targetValue = data->readTank2TargetValue();

     if (data->m_state == MachineState::Idle && data->m_percentFilledTank2 > targetValue) {
         data->setState(MachineState::Flushing);
         qDebug() << "Flushing tank 2";
         data->setTank2ValveState(true);
         data->m_machineTimer.start();
         return UA_STATUSCODE_GOOD;
     }
     else {
         qDebug() << "Unable to comply";
         return UA_STATUSCODE_BADUSERACCESSDENIED;
     }
 }

 UA_StatusCode DemoServer::resetMethod(UA_Server *server,
                                       const UA_NodeId *sessionId, void *sessionHandle,
                                       const UA_NodeId *methodId, void *methodContext,
                                       const UA_NodeId *objectId, void *objectContext,
                                       size_t inputSize, const UA_Variant *input,
                                       size_t outputSize, UA_Variant *output)
 {
     Q_UNUSED(server);
     Q_UNUSED(sessionId);
     Q_UNUSED(sessionHandle);
     Q_UNUSED(methodId);
     Q_UNUSED(objectId);
     Q_UNUSED(objectContext);
     Q_UNUSED(inputSize);
     Q_UNUSED(input);
     Q_UNUSED(outputSize);
     Q_UNUSED(output);

     DemoServer *data = static_cast<DemoServer *>(methodContext);

         qDebug() << "Reset simulation";
         data->setState(MachineState::Idle);
         data->m_machineTimer.stop();
         data->setTank2ValveState(false);
         data->setPercentFillTank1(100);
         data->setPercentFillTank2(0);
         return UA_STATUSCODE_GOOD;
 }

 void DemoServer::setState(DemoServer::MachineState state)
 {
     UA_Variant val;
     m_state = state;
     UA_Variant_setScalarCopy(&val, &state, &UA_TYPES[UA_TYPES_UINT32]);
     UA_Server_writeValue(m_server, m_machineStateNode, val);
 }

 void DemoServer::setPercentFillTank1(double fill)
 {
     UA_Variant val;
     m_percentFilledTank1 = fill;
     UA_Variant_setScalarCopy(&val, &fill, &UA_TYPES[UA_TYPES_DOUBLE]);
     UA_Server_writeValue(this->m_server, this->m_percentFilledTank1Node, val);
 }

 void DemoServer::setPercentFillTank2(double fill)
 {
     UA_Variant val;
     m_percentFilledTank2 = fill;
     UA_Variant_setScalarCopy(&val, &fill, &UA_TYPES[UA_TYPES_DOUBLE]);
     UA_Server_writeValue(this->m_server, this->m_percentFilledTank2Node, val);
 }

 void DemoServer::setTank2ValveState(bool state)
 {
     UA_Variant val;
     UA_Variant_setScalarCopy(&val, &state, &UA_TYPES[UA_TYPES_BOOLEAN]);
     UA_Server_writeValue(this->m_server, this->m_tank2ValveStateNode, val);
 }

 double DemoServer::readTank2TargetValue()
 {
     UA_Variant var;
     UA_Server_readValue(m_server, m_tank2TargetPercentNode, &var);
     return static_cast<double *>(var.data)[0];
 }

 UA_NodeId DemoServer::addMethod(const UA_NodeId &folder, const QString &variableNode,
                                 const QString &description, const QString &browseName,
                                 const QString &displayName, UA_MethodCallback cb,
                                 quint32 referenceType)
 {
     UA_NodeId methodNodeId = Open62541Utils::nodeIdFromQString(variableNode);

     UA_MethodAttributes attr = UA_MethodAttributes_default;

     attr.description = UA_LOCALIZEDTEXT_ALLOC("en-US", description.toUtf8().constData());
     attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", displayName.toUtf8().constData());
     attr.executable = true;
     UA_QualifiedName methodBrowseName = UA_QUALIFIEDNAME_ALLOC(methodNodeId.namespaceIndex,
                                                                browseName.toUtf8().constData());

     UA_NodeId resultId;
     UA_StatusCode result = UA_Server_addMethodNode(m_server, methodNodeId, folder,
                                                      UA_NODEID_NUMERIC(0, referenceType),
                                                      methodBrowseName,
                                                      attr, cb,
                                                      0, nullptr,
                                                      0, nullptr,
                                                      this, &resultId);

     UA_NodeId_clear(&methodNodeId);
     UA_MethodAttributes_clear(&attr);
     UA_QualifiedName_clear(&methodBrowseName);

     if (result != UA_STATUSCODE_GOOD) {
         qWarning() << "Could not add Method:" << result;
         return UA_NODEID_NULL;
     }
     return resultId;
 }

 void DemoServer::launch()
 {
     UA_StatusCode s = UA_Server_run_startup(m_server);
     if (s != UA_STATUSCODE_GOOD)
          qFatal("Could not launch server");
      m_running = true;
      m_timer.start();

      int ns1 = UA_Server_addNamespace(m_server, "Demo Namespace");
      if (ns1 != 2) {
          qFatal("Unexpected namespace index for Demo namespace");
      }

      UA_NodeId machineObject = addObject(QOpcUa::namespace0Id(QOpcUa::NodeIds::Namespace0::ObjectsFolder),
                                          u"ns=2;s=Machine"_s,
                                          u"Machine"_s,
                                          u"Machine"_s,
                                          u"The machine simulator"_s,
                                          UA_NS0ID_ORGANIZES);

      UA_NodeId tank1Object = addObject(u"ns=2;s=Machine"_s,
                                        u"ns=2;s=Machine.Tank1"_s,
                                        u"Tank1"_s,
                                        u"Tank 1"_s);

      UA_NodeId tank2Object = addObject(u"ns=2;s=Machine"_s,
                                        u"ns=2;s=Machine.Tank2"_s,
                                        u"Tank2"_s,
                                        u"Tank 2"_s);

      m_percentFilledTank1Node = addVariable(tank1Object,
                                             u"ns=2;s=Machine.Tank1.PercentFilled"_s,
                                             u"PercentFilled"_s,
                                             u"Tank 1 Fill Level"_s,
                                             100.0,
                                             QOpcUa::Types::Double);

      m_percentFilledTank2Node = addVariable(tank2Object,
                                             u"ns=2;s=Machine.Tank2.PercentFilled"_s,
                                             u"PercentFilled"_s,
                                             u"Tank 2 Fill Level"_s,
                                             0.0,
                                             QOpcUa::Types::Double);

      m_tank2TargetPercentNode = addVariable(tank2Object,
                                             u"ns=2;s=Machine.Tank2.TargetPercent"_s,
                                             u"TargetPercent"_s,
                                             u"Tank 2 Target Level"_s,
                                             0.0,
                                             QOpcUa::Types::Double);

      m_tank2ValveStateNode = addVariable(tank2Object,
                                          u"ns=2;s=Machine.Tank2.ValveState"_s,
                                          u"ValveState"_s,
                                          u"Tank 2 Valve State"_s,
                                          false,
                                          QOpcUa::Types::Boolean);

      m_machineStateNode = addVariable(machineObject,
                                       u"ns=2;s=Machine.State"_s,
                                       u"State"_s,
                                       u"Machine State"_s,
                                       static_cast<quint32>(MachineState::Idle),
                                       QOpcUa::Types::UInt32);

      UA_NodeId tempId;
      tempId = addVariable(machineObject,
                           u"ns=2;s=Machine.Designation"_s,
                           u"Designation"_s,
                           u"Machine Designation"_s,
                           u"TankExample"_s,
                           QOpcUa::Types::String);
      UA_NodeId_clear(&tempId);

      tempId = addMethod(machineObject,
                         u"ns=2;s=Machine.Start"_s,
                         u"Starts the pump"_s,
                         u"Start"_s,
                         u"Start Pump"_s,
                         &startPumpMethod);
      UA_NodeId_clear(&tempId);

      tempId = addMethod(machineObject,
                         u"ns=2;s=Machine.Stop"_s,
                         u"Stops the pump"_s,
                         u"Stop"_s,
                         u"Stop Pump"_s,
                         &stopPumpMethod);
      UA_NodeId_clear(&tempId);

      tempId = addMethod(machineObject,
                         u"ns=2;s=Machine.FlushTank2"_s,
                         u"Flushes tank 2"_s,
                         u"FlushTank2"_s,
                         u"Flush Tank 2"_s,
                         &flushTank2Method);
      UA_NodeId_clear(&tempId);

      tempId = addMethod(machineObject,
                         u"ns=2;s=Machine.Reset"_s,
                         u"Resets the simulation"_s,
                         u"Reset"_s,
                         u"Reset Simulation"_s,
                         &resetMethod);
      UA_NodeId_clear(&tempId);

      UA_NodeId_clear(&machineObject);
      UA_NodeId_clear(&tank1Object);
      UA_NodeId_clear(&tank2Object);

      QObject::connect(&m_machineTimer, &QTimer::timeout, this, [this]() {

          double targetValue = readTank2TargetValue();
          if (m_state == MachineState::Pumping
              && m_percentFilledTank1 > 0
              && m_percentFilledTank2 < targetValue) {
             setPercentFillTank1(m_percentFilledTank1 - 1);
             setPercentFillTank2(m_percentFilledTank2 + 1);
             if (qFuzzyIsNull(m_percentFilledTank1) || m_percentFilledTank2 >= targetValue) {
                 setState(MachineState::Idle);
                 m_machineTimer.stop();
             }
          } else if (m_state == MachineState::Flushing && m_percentFilledTank2 > targetValue) {
              setPercentFillTank2(m_percentFilledTank2 - 1);
              if (m_percentFilledTank2 <= targetValue) {
                  setTank2ValveState(false);
                  setState(MachineState::Idle);
                  m_machineTimer.stop();
              }
          }
      });
 }