From d7d1be4ca95afa688caae680915dda63b189c9ef Mon Sep 17 00:00:00 2001
From: kongr45gpen <electrovesta@gmail.com>
Date: Sun, 4 Sep 2022 18:29:51 +0000
Subject: [PATCH] Define lazy, function-based parameters

---
 inc/ErrorHandler.hpp                          |  12 ++
 inc/Helpers/LazyParameter.hpp                 | 105 ++++++++++++++++++
 inc/Helpers/Parameter.hpp                     |  26 +++--
 inc/etl/String.hpp                            |   2 +-
 lib/etl                                       |   2 +-
 test/Parameters/LazyParameterTests.cpp        |  83 ++++++++++++++
 .../ParameterTests.cpp}                       |   0
 7 files changed, 216 insertions(+), 14 deletions(-)
 create mode 100644 inc/Helpers/LazyParameter.hpp
 create mode 100644 test/Parameters/LazyParameterTests.cpp
 rename test/{Services/Parameter.cpp => Parameters/ParameterTests.cpp} (100%)

diff --git a/inc/ErrorHandler.hpp b/inc/ErrorHandler.hpp
index 351a75ca..e9b341bf 100644
--- a/inc/ErrorHandler.hpp
+++ b/inc/ErrorHandler.hpp
@@ -344,6 +344,18 @@ public:
 		 * ID is not controlled by the Service (ST[14])
 		 */
 		NotControlledApplication = 50,
+		/**
+		 * Parameter is requested, but the provider of the parameter value does not exist yet
+		 */
+		ParameterValueMissing = 51,
+		/**
+		 * Attempted to write to a read-only parameter
+		 */
+		ParameterReadOnly = 52,
+		/**
+		 * Attempted to read from a write-only parameter
+		 */
+		ParameterWriteOnly = 53,
 	};
 
 	/**
diff --git a/inc/Helpers/LazyParameter.hpp b/inc/Helpers/LazyParameter.hpp
new file mode 100644
index 00000000..d28c3baf
--- /dev/null
+++ b/inc/Helpers/LazyParameter.hpp
@@ -0,0 +1,105 @@
+#ifndef ECSS_SERVICES_LAZYPARAMETER_HPP
+#define ECSS_SERVICES_LAZYPARAMETER_HPP
+
+#include <etl/optional.h>
+#include <functional>
+#include "Parameter.hpp"
+
+/**
+ * A Lazy Parameter is a ParameterService parameter that does not keep a value in
+ * memory, but calls an external function to fetch a new value whenever needed.
+ *
+ * The LazyParameter allows its users to call expensive value-fetching operators
+ * ONLY when a value is requested. This prevents having to update a value in
+ * memory every so often.
+ *
+ * This "lazy" fetching is useful when it is expensive (in terms of time, power
+ * etc.) to get updated values, e.g. from peripherals or difficult calculations.
+ *
+ * @warning This class is NOT re-entrant. The developer will have to make sure
+ * that only one thread has access to it at a time, otherwise undefined behaviour
+ * will occur.
+ *
+ * @tparam DataType The data type of the parameter's value
+ */
+template <typename DataType>
+class LazyParameter : public ParameterBase {
+public:
+	/**
+	 * The type of the function that returns the current value of this parameter.
+	 */
+	using Getter = std::function<DataType()>;
+
+	/**
+	 * LazyParameter constructor without a getter function.
+	 *
+	 * When a getter function is not present, an error may be shown when fetching a value
+	 * for the parameter. A @p fallback value will be returned in this case, if needed.
+	 * @param fallback
+	 */
+	explicit LazyParameter(const DataType& fallback = 0) : fallback(fallback) {}
+
+	/**
+	 * LazyParameter constructor with a pre-defined getter function
+	 * @param getter
+	 * @param fallback
+	 */
+	explicit LazyParameter(const Getter& getter, const DataType& fallback = 0) : getter(getter), fallback(fallback) {}
+
+	/**
+	 * Set a getter function for this parameter. The getter function is called
+	 * whenever a value for this parameter is requested.
+	 */
+	void setGetter(const Getter& _getter) {
+		LazyParameter::getter = _getter;
+	}
+
+	/**
+	 * Remove the getter function of this parameter.
+	 */
+	void unsetGetter() {
+		getter.reset();
+	}
+
+	/**
+	 * Get the current value of this parameter, if the getter is defined.
+	 *
+	 * @note This function may take some time to return a value, since it calls
+	 * the "expensive" getter function.
+	 */
+	etl::optional<DataType> getValue() {
+		if (getter) {
+			return (*getter)();
+		} else {
+			return {};
+		}
+	}
+
+	inline double getValueAsDouble() override {
+		if constexpr (std::is_arithmetic_v<DataType>) {
+			return static_cast<double>(getValue().value_or(fallback));
+		} else {
+			return 0;
+		}
+	}
+
+	inline void appendValueToMessage(Message& message) override {
+		if (getter) {
+			message.append<DataType>((*getter)());
+		} else {
+			message.append<DataType>(fallback);
+			ErrorHandler::reportError(message, ErrorHandler::ParameterValueMissing);
+		}
+	};
+
+	inline void setValueFromMessage(Message& message) override {
+		[[maybe_unused]] auto skippedBytes = message.read<DataType>();
+		ErrorHandler::reportError(message, ErrorHandler::ParameterReadOnly);
+	};
+private:
+	etl::optional<Getter> getter;
+	DataType fallback;
+};
+
+
+#endif //ECSS_SERVICES_LAZYPARAMETER_HPP
diff --git a/inc/Helpers/Parameter.hpp b/inc/Helpers/Parameter.hpp
index 3ce19915..60241984 100644
--- a/inc/Helpers/Parameter.hpp
+++ b/inc/Helpers/Parameter.hpp
@@ -32,8 +32,22 @@
  */
 class ParameterBase {
 public:
+	/**
+	 * Given an ECSS message that contains this parameter as its first input, this loads the value from that parameter
+	 */
 	virtual void appendValueToMessage(Message& message) = 0;
+
+	/**
+	 * Appends the parameter as an ECSS value to an ECSS Message
+	 */
 	virtual void setValueFromMessage(Message& message) = 0;
+
+	/**
+	 * Converts the value of a parameter to a double.
+	 *
+	 * Some precision may be lost in the process. If the value is not arithmetic,
+	 * then usually 0 is returned.
+	 */
 	virtual double getValueAsDouble() = 0;
 };
 
@@ -58,12 +72,6 @@ public:
 		return currentValue;
 	}
 
-	/**
-	 * Converts the value of a parameter to a double.
-	 *
-	 * Some precision may be lost in the process. If the value is not arithmetic,
-	 * then 0 is returned.
-	 */
 	inline double getValueAsDouble() override {
 		if constexpr (std::is_arithmetic_v<DataType>) {
 			return static_cast<double>(currentValue);
@@ -72,16 +80,10 @@ public:
 		}
 	}
 
-	/**
-	 * Given an ECSS message that contains this parameter as its first input, this loads the value from that paremeter
-	 */
 	inline void setValueFromMessage(Message& message) override {
 		currentValue = message.read<DataType>();
 	};
 
-	/**
-	 * Appends the parameter as an ECSS value to an ECSS Message
-	 */
 	inline void appendValueToMessage(Message& message) override {
 		message.append<DataType>(currentValue);
 	};
diff --git a/inc/etl/String.hpp b/inc/etl/String.hpp
index 7691f783..c97573e9 100644
--- a/inc/etl/String.hpp
+++ b/inc/etl/String.hpp
@@ -2,7 +2,7 @@
 #define ECSS_SERVICES_ETL_STRING_HPP
 
 #include <cstddef>
-#include <etl/cstring.h>
+#include <etl/string.h>
 
 /**
  * A fixed-size string
diff --git a/lib/etl b/lib/etl
index 5a7e1811..fce2b5ca 160000
--- a/lib/etl
+++ b/lib/etl
@@ -1 +1 @@
-Subproject commit 5a7e181100dae73659db133783ae964c7661437b
+Subproject commit fce2b5ca8d2a139e0aefb450773b82e2cdfaa55f
diff --git a/test/Parameters/LazyParameterTests.cpp b/test/Parameters/LazyParameterTests.cpp
new file mode 100644
index 00000000..800c865c
--- /dev/null
+++ b/test/Parameters/LazyParameterTests.cpp
@@ -0,0 +1,83 @@
+#include "../Services/ServiceTests.hpp"
+#include "Helpers/LazyParameter.hpp"
+#include "Message.hpp"
+#include "catch2/catch_all.hpp"
+
+TEST_CASE("Lazy Parameter: Getter") {
+	LazyParameter<uint32_t> parameter;
+	CHECK(parameter.getValue().has_value() == false);
+
+	parameter.setGetter([]() -> auto{
+		static uint32_t value = 99100;
+		return value++;
+	});
+
+	CHECK(parameter.getValue().value() == 99100);
+	CHECK(parameter.getValue().value() == 99101);
+	CHECK(parameter.getValueAsDouble() == 99102);
+
+	parameter.unsetGetter();
+
+	CHECK(parameter.getValue().has_value() == false);
+	CHECK(parameter.getValueAsDouble() == 0);
+}
+
+TEST_CASE("Lazy Parameter: Messages") {
+	const int16_t fallback = -1;
+	const int16_t value = 42;
+
+	LazyParameter<int16_t> parameter(
+	    []() -> auto{
+		    return value;
+	    },
+	    fallback);
+
+	SECTION("Getter works") {
+		Message message(0, 0, Message::TC);
+		parameter.appendValueToMessage(message);
+
+		CHECK(message.dataSize == 2);
+		CHECK(message.readSint16() == value);
+		CHECK(ServiceTests::hasNoErrors());
+	}
+
+	SECTION("Getter broken") {
+		parameter.unsetGetter();
+
+		Message message(0, 0, Message::TC);
+		parameter.appendValueToMessage(message);
+
+		CHECK(message.dataSize == 2);
+		CHECK(message.readSint16() == fallback);
+		CHECK(ServiceTests::thrownError(ErrorHandler::ParameterValueMissing));
+	}
+
+	SECTION("Attempt to write") {
+		Message message(0, 0, Message::TC);
+		message.appendSint16(99);
+		message.appendSint16(100);
+
+		parameter.setValueFromMessage(message);
+		CHECK(ServiceTests::thrownError(ErrorHandler::ParameterReadOnly));
+
+		// Make sure that the appropriate amount of bytes has been skipped
+		CHECK(message.readSint16() == 100);
+	}
+}
+
+TEST_CASE("Lazy Parameter: Complex Types") {
+	LazyParameter<Time::CustomCUC_t> parameter({100});
+
+	SECTION("Default value") {
+		CHECK(parameter.getValue().has_value() == false);
+	}
+
+	SECTION("Set value") {
+		parameter.setGetter([]() -> Time::CustomCUC_t {
+			return {200};
+		});
+
+		CHECK(parameter.getValue().value() == Time::CustomCUC_t{200});
+		CHECK(parameter.getValueAsDouble() == 0);
+	}
+}
diff --git a/test/Services/Parameter.cpp b/test/Parameters/ParameterTests.cpp
similarity index 100%
rename from test/Services/Parameter.cpp
rename to test/Parameters/ParameterTests.cpp
-- 
GitLab