From af3cb3c3bb5ab33952a0c68a7b64147a4b3bca4e Mon Sep 17 00:00:00 2001
From: d-rothe <39566710+d-rothe@users.noreply.github.com>
Date: Wed, 15 Jun 2022 11:23:36 +0200
Subject: [PATCH] Wip/9838 error reporting concept (#73)

* concept for error reporting to set_error
- destination is DOOCS EqFct::set_error(code, message)
- StatusMonitor variables and/or Device status variables as input
- configuration per location via xml
- only single data source for set_error
- consistent updates of error code and error message, to remove
duplicate log lines
this depends on changes in ApplicationCore/DeviceModule!
* remove deprecated use of boost/test/test_case_template.hpp
* make configuration of status message input unneccesary
it will be found from the naming convention
* some refactoring
which was neccesary because Status definitions moved to
ControlSystemAdapter
* add docu for set_error
* add basic test for set_error tag

Co-authored-by: Dietrich Rothe <dietrich.rothe@desy.de>
---
 doc/mainpage.dox                              | 14 +++-
 include/CSAdapterEqFct.h                      | 13 ++--
 include/PropertyDescription.h                 |  7 ++
 include/StatusHandler.h                       | 37 +++++++++++
 include/VariableMapper.h                      |  5 ++
 src/CSAdapterEqFct.cc                         | 17 +++++
 src/StatusHandler.cc                          | 65 +++++++++++++++++++
 src/VariableMapper.cc                         | 21 ++++++
 ...serverTestSetError-DoocsVariableConfig.xml |  9 +++
 tests/serverTestSetError.conf                 | 17 +++++
 tests/src/serverTestSetError.cpp              | 49 ++++++++++++++
 tests/src/testDoocsPVFactory.cpp              |  2 +-
 tests/src/testDoocsProcessScalar.cpp          |  2 +-
 tests/src/testDoocsSpectrum.cpp               |  2 +-
 14 files changed, 248 insertions(+), 12 deletions(-)
 create mode 100644 include/StatusHandler.h
 create mode 100644 src/StatusHandler.cc
 create mode 100644 tests/serverTestSetError-DoocsVariableConfig.xml
 create mode 100644 tests/serverTestSetError.conf
 create mode 100644 tests/src/serverTestSetError.cpp

diff --git a/doc/mainpage.dox b/doc/mainpage.dox
index 764a719..1d50c04 100644
--- a/doc/mainpage.dox
+++ b/doc/mainpage.dox
@@ -151,7 +151,7 @@ The `D_array` tag takes the following arguments through XML attributes of the D_
 
 For an example, see above in Section \ref mapping_file.
 
-\subsection D_xy D_xy
+\subsubsection D_xy D_xy
 The `D_xy` tag takes the following arguments through properties:
 - `x_source` : Name of process variable which should be used for x-axis values
 - `y_source` : Name of process variable which should be used for y-axis values
@@ -162,8 +162,18 @@ The `D_xy` tag takes the following arguments through sub-tags:
 
 \include tests/serverTestXy-DoocsVariableConfig.xml
 
+\subsection set_error Error reporting
+The special tag `set_error` is allowed only once per location. XML attributes:
+- `statusCodeSource` : Path to the process variable which should be used for reading error/status codes.
+
+DoocsAdapter will automatically look for an associated variable with error/status messages, and if present, read both
+consistently.
+Errors indicated via the status variable will be published via DOOCS set_error function (of the location), which
+sets the properties ERROR, ERROR.STR, STS.ERROR, STS.NEWERROR, and also logs into LOG, LOG.LAST, LOG_HTML and LOG_HTML.LAST.
+Compare DOOCS documentation for error properties and propagation to the overall error counting per server, SVR.ERROR_COUNT.
+
           
-\subsection specifie_code Specifie `eq_fct_code` in the mapping file
+\subsection specifie_code Specify `eq_fct_code` in the mapping file
 
 It is possible to specify the `eq_fct_code` which must match `eq_fct_type` in the configuration file with `code` in 
 the \<location\> tag of the mapping file.
diff --git a/include/CSAdapterEqFct.h b/include/CSAdapterEqFct.h
index f1e9ed8..a7dd6a8 100644
--- a/include/CSAdapterEqFct.h
+++ b/include/CSAdapterEqFct.h
@@ -1,5 +1,4 @@
-#ifndef CS_ADAPTER_EQ_FCT_H
-#define CS_ADAPTER_EQ_FCT_H
+#pragma once
 
 #include <boost/noncopyable.hpp>
 #include <boost/scoped_ptr.hpp>
@@ -9,13 +8,13 @@
 #include <ChimeraTK/ControlSystemAdapter/ControlSystemPVManager.h>
 #include <ChimeraTK/ControlSystemAdapter/ProcessVariable.h>
 
-#include "DoocsUpdater.h"
-#include "PropertyDescription.h"
-
 namespace ChimeraTK {
 
   template<typename DOOCS_T, typename DOOCS_PRIMITIVE_T>
   class DoocsProcessArray;
+  class StatusHandler;
+  class DoocsUpdater;
+  class PropertyDescription;
 
   class CSAdapterEqFct : public EqFct, boost::noncopyable {
    protected:
@@ -28,6 +27,8 @@ namespace ChimeraTK {
     static bool emptyLocationVariablesHandled;
     boost::shared_ptr<DoocsUpdater> updater_;
 
+    boost::shared_ptr<StatusHandler> statusHandler_;
+
    public:
     // The fctName (location name ) is usually coming from the config file and
     // should be left empty. Only for testing without actually running a DOOCS
@@ -48,5 +49,3 @@ namespace ChimeraTK {
   };
 
 } // namespace ChimeraTK
-
-#endif // CS_ADAPTER_EQ_FCT_H
diff --git a/include/PropertyDescription.h b/include/PropertyDescription.h
index ddc07f2..91247f0 100644
--- a/include/PropertyDescription.h
+++ b/include/PropertyDescription.h
@@ -215,6 +215,13 @@ namespace ChimeraTK {
       useDataMatchingDefault(useDataMatchingDefault_) {}
   };
 
+  // parsed info about error reporting to locations
+  struct ErrorReportingInfo {
+    ChimeraTK::RegisterPath statusCodeSource;
+    ChimeraTK::RegisterPath statusStringSource;
+    std::string targetLocation;
+  };
+
 } // namespace ChimeraTK
 
 #endif // CHIMERATK_DOOCS_ADAPTER_PROPERTY_DESCRIPTION_H
diff --git a/include/StatusHandler.h b/include/StatusHandler.h
new file mode 100644
index 0000000..9eba4c0
--- /dev/null
+++ b/include/StatusHandler.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include <ChimeraTK/ControlSystemAdapter/StatusWithMessageReader.h>
+#include <boost/shared_ptr.hpp>
+#include <boost/noncopyable.hpp>
+
+class EqFct;
+namespace ChimeraTK {
+
+  class DoocsAdapter;
+  class DoocsUpdater;
+
+  /**
+ * StatusHandler implements an update function for reporting errors to locations.
+ * Input should be e.g. status of a DeviceModule or StatusAggregator. 
+ * Status codes are mapped to DOOCS and written via set_error(code, string)
+ */
+  class StatusHandler : public boost::noncopyable {
+    boost::shared_ptr<DoocsUpdater> _doocsUpdater;
+    EqFct* _eqFct;
+    /// accessors to the status and string variables we want to monitor
+    StatusWithMessageReader _varPair;
+
+   public:
+    void updateError(TransferElementID transferElementId);
+
+    /// statusScalar and statusString are the variables to monitor. statusScalar is mandatory, statusString not.
+    /// If both are set, updates must come in consistently
+    StatusHandler(EqFct* eqFct, boost::shared_ptr<DoocsUpdater> const& updater,
+        boost::shared_ptr<ChimeraTK::NDRegisterAccessor<int32_t>> const& statusScalar,
+        boost::shared_ptr<ChimeraTK::NDRegisterAccessor<std::string>> const& statusString = nullptr);
+
+    /// mapping function from Device.status/StatusOutput to DOOCS error codes
+    int statusCodeMapping(int x);
+  };
+
+} // namespace ChimeraTK
diff --git a/include/VariableMapper.h b/include/VariableMapper.h
index f9c0a93..2d05d0b 100644
--- a/include/VariableMapper.h
+++ b/include/VariableMapper.h
@@ -56,6 +56,8 @@ namespace ChimeraTK {
 
     const std::set<std::string>& getUsedVariables() const { return _usedInputVariables; }
 
+    const std::list<ErrorReportingInfo>& getErrorReportingInfos() const { return _errorReportingInfos; }
+
    protected:
     VariableMapper() = default;
 
@@ -65,11 +67,14 @@ namespace ChimeraTK {
     std::set<std::string> _usedInputVariables; // For tracing which variables are
                                                // not to be imported.
 
+    std::list<ErrorReportingInfo> _errorReportingInfos;
+
     void processLocationNode(xmlpp::Node const* locationNode);
     void processNode(xmlpp::Node const* propertyNode, std::string locationName);
     void processSpectrumNode(xmlpp::Node const* node, std::string locationName);
     void processXyNode(xmlpp::Node const* node, std::string& locationName);
     void processIfffNode(xmlpp::Node const* node, std::string& locationName);
+    void processSetErrorNode(xmlpp::Node const* node, std::string& locationName);
     void processImportNode(xmlpp::Node const* importNode, std::string importLocationName = std::string());
     void processCode(xmlpp::Element const* location, std::string locationName);
 
diff --git a/src/CSAdapterEqFct.cc b/src/CSAdapterEqFct.cc
index c2046a4..33f06e3 100644
--- a/src/CSAdapterEqFct.cc
+++ b/src/CSAdapterEqFct.cc
@@ -1,8 +1,10 @@
+#include "StatusHandler.h" // include this first to avoid name clash with #define from DOOCS
 #include "CSAdapterEqFct.h"
 #include "DoocsPVFactory.h"
 #include "DoocsUpdater.h"
 #include "VariableMapper.h"
 #include "DoocsProcessArray.h"
+#include "PropertyDescription.h"
 
 namespace ChimeraTK {
 
@@ -22,6 +24,21 @@ namespace ChimeraTK {
       name_.assign(fctName);
     }
     registerProcessVariablesInDoocs();
+
+    // construct and populate the StatusHandler for this location
+    for(const ErrorReportingInfo& errorReportingInfo :
+        ChimeraTK::VariableMapper::getInstance().getErrorReportingInfos()) {
+      if(name_.get_value() != errorReportingInfo.targetLocation) continue;
+
+      assert(!statusHandler_); // only single StatusHandler may be requested per location
+      auto statusCodeVariable = controlSystemPVManager_->getProcessArray<int32_t>(errorReportingInfo.statusCodeSource);
+      if(!statusCodeVariable)
+        throw ChimeraTK::logic_error("illegal/non-existing statusCodeSource: " + errorReportingInfo.statusCodeSource);
+      // this one is optional
+      ProcessArray<std::string>::SharedPtr statusStringVariable =
+          controlSystemPVManager_->getProcessArray<std::string>(errorReportingInfo.statusStringSource);
+      statusHandler_.reset(new StatusHandler(this, updater_, statusCodeVariable, statusStringVariable));
+    }
   }
 
   CSAdapterEqFct::~CSAdapterEqFct() {
diff --git a/src/StatusHandler.cc b/src/StatusHandler.cc
new file mode 100644
index 0000000..96c5b2f
--- /dev/null
+++ b/src/StatusHandler.cc
@@ -0,0 +1,65 @@
+
+// name clash with DOOCS-#defined FAULT, so include this first and then #undef
+#include <eq_errors.h>
+#include <eq_fct.h>
+#undef FAULT
+
+#include "StatusHandler.h"
+
+#include "DoocsUpdater.h"
+#include "DoocsAdapter.h"
+#include <ChimeraTK/ScalarRegisterAccessor.h>
+#include <ChimeraTK/DataConsistencyGroup.h>
+#include <ChimeraTK/ControlSystemAdapter/StatusWithMessageReader.h>
+#include <boost/shared_ptr.hpp>
+
+namespace ChimeraTK {
+
+  void StatusHandler::updateError(TransferElementID transferElementId) {
+    if(!_varPair.update(transferElementId)) return;
+    int statusCode = _varPair._status;
+    int err_no = statusCodeMapping(statusCode);
+    std::string err_str = _varPair.getMessage();
+
+    // Note: we already own the location lock by specification of the DoocsUpdater
+    if(err_no == no_error)
+      _eqFct->set_error(err_no);
+    else
+      _eqFct->set_error(err_no, err_str);
+  }
+
+  StatusHandler::StatusHandler(EqFct* eqFct, boost::shared_ptr<DoocsUpdater> const& updater,
+      boost::shared_ptr<ChimeraTK::NDRegisterAccessor<int32_t>> const& statusScalar,
+      boost::shared_ptr<ChimeraTK::NDRegisterAccessor<std::string>> const& statusString)
+  : _doocsUpdater(updater), _eqFct(eqFct), _varPair(ChimeraTK::ScalarRegisterAccessor<int32_t>(statusScalar)) {
+    if(!statusScalar->isReadable()) throw ChimeraTK::logic_error(statusScalar->getName() + " is not readable!");
+
+    _doocsUpdater->addVariable(
+        _varPair._status, _eqFct, std::bind(&StatusHandler::updateError, this, statusScalar->getId()));
+    if(statusString) {
+      if(!statusString->isReadable()) throw ChimeraTK::logic_error(statusString->getName() + " is not readable!");
+      _varPair.setMessageSource(ChimeraTK::ScalarRegisterAccessor<std::string>(statusString));
+
+      _doocsUpdater->addVariable(
+          _varPair._message, _eqFct, std::bind(&StatusHandler::updateError, this, statusString->getId()));
+    }
+  }
+
+  int StatusHandler::statusCodeMapping(int x) {
+    auto err = StatusAccessorBase::Status(x);
+    // a mapping for StatusOutput values -> DOOCS error codes
+    switch(err) {
+      case StatusAccessorBase::Status::OK:
+        return no_error;
+      case StatusAccessorBase::Status::FAULT:
+        return not_available;
+      case StatusAccessorBase::Status::OFF:
+        return device_offline;
+      case StatusAccessorBase::Status::WARNING:
+        return warning;
+      default:
+        return x;
+    }
+  }
+
+} // namespace ChimeraTK
diff --git a/src/VariableMapper.cc b/src/VariableMapper.cc
index 7121a57..137ec49 100644
--- a/src/VariableMapper.cc
+++ b/src/VariableMapper.cc
@@ -85,6 +85,9 @@ namespace ChimeraTK {
       else if(node->get_name() == "D_ifff") {
         processIfffNode(node, locationName);
       }
+      else if(node->get_name() == "set_error") {
+        processSetErrorNode(node, locationName);
+      }
       else {
         throw std::invalid_argument(std::string("Error parsing xml file in location ") + locationName +
             ": Unknown node '" + node->get_name() + "'");
@@ -386,6 +389,24 @@ namespace ChimeraTK {
     addDescription(ifffDescription, absoluteSources);
   }
 
+  /********************************************************************************************************************/
+  void VariableMapper::processSetErrorNode(xmlpp::Node const* node, std::string& locationName) {
+    for(auto const& errRepInfo : _errorReportingInfos) {
+      if(errRepInfo.targetLocation == locationName)
+        throw std::invalid_argument(std::string("Error parsing xml file in location ") + locationName +
+            ": tag <set_error> is allowed only once per location.");
+    }
+    ErrorReportingInfo errInfo;
+    errInfo.targetLocation = locationName;
+
+    auto setErrorXml = asXmlElement(node);
+    std::string s = getAttributeValue(setErrorXml, "statusCodeSource");
+    errInfo.statusCodeSource = s;
+    // matching status message source is found automatically by naming convention - if it exists
+    errInfo.statusStringSource = s + "_message";
+    _errorReportingInfos.push_back(errInfo);
+  }
+
   /********************************************************************************************************************/
 
   void VariableMapper::processImportNode(xmlpp::Node const* importNode, std::string importLocationName) {
diff --git a/tests/serverTestSetError-DoocsVariableConfig.xml b/tests/serverTestSetError-DoocsVariableConfig.xml
new file mode 100644
index 0000000..f2b8161
--- /dev/null
+++ b/tests/serverTestSetError-DoocsVariableConfig.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<device_server xmlns="https://github.com/ChimeraTK/ControlSystemAdapter-DoocsAdapter">
+    <location name="INT">
+        <set_error statusCodeSource="/INT/FROM_DEVICE_SCALAR" />
+        <D_spectrum name="TEST" source="/FLOAT/FROM_DEVICE_ARRAY" />
+    </location>
+
+    <import>/</import>
+</device_server>
diff --git a/tests/serverTestSetError.conf b/tests/serverTestSetError.conf
new file mode 100644
index 0000000..5cf1901
--- /dev/null
+++ b/tests/serverTestSetError.conf
@@ -0,0 +1,17 @@
+eq_conf:
+
+oper_uid:       -1
+oper_gid:       405
+xpert_uid:      1000
+xpert_gid:      1000
+ring_buffer:    10000
+memory_buffer:  500
+
+eq_fct_name:    "ERROR_TEST._SVR"
+eq_fct_type:    1
+{
+SVR.RPC_NUMBER:         700000004
+SVR.NAME:       "ERROR_TEST._SVR"
+SVR.BPN:        6000
+}
+
diff --git a/tests/src/serverTestSetError.cpp b/tests/src/serverTestSetError.cpp
new file mode 100644
index 0000000..454823d
--- /dev/null
+++ b/tests/src/serverTestSetError.cpp
@@ -0,0 +1,49 @@
+#define BOOST_TEST_MODULE serverTestSetError
+
+#include <boost/test/included/unit_test.hpp>
+
+#include <ChimeraTK/ControlSystemAdapter/Testing/ReferenceTestApplication.h>
+
+extern const char* object_name;
+#include <doocs-server-test-helper/ThreadedDoocsServer.h>
+#include <doocs-server-test-helper/doocsServerTestHelper.h>
+
+#include <eq_fct.h>
+
+#include "DoocsAdapter.h"
+#include "serverBasedTestTools.h"
+
+using namespace boost::unit_test_framework;
+using namespace ChimeraTK;
+
+DOOCS_ADAPTER_DEFAULT_FIXTURE_STATIC_APPLICATION
+
+std::string lastErrString = "(unset)";
+
+// basic check for error code handling, without associated message source
+BOOST_AUTO_TEST_CASE(testErrorNoMessageSource) {
+  auto check = [] {
+    auto location = getLocationFromPropertyAddress("//INT/FROM_DEVICE_SCALAR");
+    location->lock();
+    int code = location->get_error();
+    lastErrString = location->get_errorstr();
+    location->unlock();
+
+    return static_cast<eq_errors>(code);
+  };
+  // Testing initial state
+  GlobalFixture::referenceTestApplication.dataValidity = DataValidity::ok;
+  DoocsServerTestHelper::doocsSet<int>("//INT/TO_DEVICE_SCALAR", 0);
+
+  GlobalFixture::referenceTestApplication.runMainLoopOnce();
+  checkWithTimeout<eq_errors>(check, no_error);
+  BOOST_CHECK(lastErrString == "ok");
+
+  // Check fault state
+  GlobalFixture::referenceTestApplication.dataValidity = DataValidity::faulty;
+  DoocsServerTestHelper::doocsSet<int>("//INT/TO_DEVICE_SCALAR", 1);
+  GlobalFixture::referenceTestApplication.runMainLoopOnce();
+  checkWithTimeout<eq_errors>(check, not_available);
+  // there must be a generic error message
+  BOOST_CHECK(lastErrString != "ok");
+}
diff --git a/tests/src/testDoocsPVFactory.cpp b/tests/src/testDoocsPVFactory.cpp
index f4eec6f..dfe213c 100644
--- a/tests/src/testDoocsPVFactory.cpp
+++ b/tests/src/testDoocsPVFactory.cpp
@@ -3,7 +3,7 @@
 // Only after defining the name include the unit test header.
 #include <boost/mpl/list.hpp>
 #include <boost/test/included/unit_test.hpp>
-#include <boost/test/test_case_template.hpp>
+#include <boost/test/unit_test.hpp>
 
 #include <sstream>
 #include <typeinfo>
diff --git a/tests/src/testDoocsProcessScalar.cpp b/tests/src/testDoocsProcessScalar.cpp
index 45e252d..62c3ada 100644
--- a/tests/src/testDoocsProcessScalar.cpp
+++ b/tests/src/testDoocsProcessScalar.cpp
@@ -2,7 +2,7 @@
 // Only after defining the name include the unit test header.
 #include <boost/mpl/list.hpp>
 #include <boost/test/included/unit_test.hpp>
-#include <boost/test/test_case_template.hpp>
+#include <boost/test/unit_test.hpp>
 
 #include "DoocsProcessScalar.h"
 #include "DoocsUpdater.h"
diff --git a/tests/src/testDoocsSpectrum.cpp b/tests/src/testDoocsSpectrum.cpp
index 34f7c6d..e54ce86 100644
--- a/tests/src/testDoocsSpectrum.cpp
+++ b/tests/src/testDoocsSpectrum.cpp
@@ -2,7 +2,7 @@
 // Only after defining the name include the unit test header.
 #include <boost/mpl/list.hpp>
 #include <boost/test/included/unit_test.hpp>
-#include <boost/test/test_case_template.hpp>
+#include <boost/test/unit_test.hpp>
 
 #include "DoocsSpectrum.h"
 #include <ChimeraTK/ControlSystemAdapter/ControlSystemPVManager.h>
-- 
GitLab