diff --git a/.changesets/volatile-protection-sources.md b/.changesets/volatile-protection-sources.md new file mode 100644 index 000000000..e039c26b6 --- /dev/null +++ b/.changesets/volatile-protection-sources.md @@ -0,0 +1,6 @@ +release: patch +summary: Allow protections to read volatile sample sources + +Protection sources now preserve the storage cv-qualification through a reference source wrapper, +so protections can be declared directly over volatile variables while rule evaluation still uses +plain sample values. diff --git a/Inc/ST-LIB_HIGH/Protections/Protection.hpp b/Inc/ST-LIB_HIGH/Protections/Protection.hpp index ec9b01999..9cf655183 100644 --- a/Inc/ST-LIB_HIGH/Protections/Protection.hpp +++ b/Inc/ST-LIB_HIGH/Protections/Protection.hpp @@ -488,11 +488,17 @@ inline RuleModel make_rule_model(const RuleDefinition& definition) { } } -template class Protection { +template > +class Protection { public: + static_assert( + std::same_as, T>, + "Protection source value_type must match the protection sample type" + ); + Protection( const char* name, - SampleSource source, + Source source, const std::array, RuleCount>& definitions ) : name(name), source(source), definitions(definitions), @@ -550,7 +556,7 @@ template class Protection { } const char* name{nullptr}; - SampleSource source; + Source source; std::array, RuleCount> definitions{}; std::array, RuleCount> rules{}; uint64_t last_fault_publish_tick{0}; diff --git a/Inc/ST-LIB_HIGH/Protections/ProtectionEngine.hpp b/Inc/ST-LIB_HIGH/Protections/ProtectionEngine.hpp index b58c480f7..6e06f1a26 100644 --- a/Inc/ST-LIB_HIGH/Protections/ProtectionEngine.hpp +++ b/Inc/ST-LIB_HIGH/Protections/ProtectionEngine.hpp @@ -12,17 +12,14 @@ namespace Protections { namespace detail { template -concept SampleSourceLike = requires(const std::remove_cvref_t& source) { - typename std::remove_cvref_t::value_type; - source.read(); -}; +concept SampleSourceLike = ReadableSampleSource; template consteval auto sample_type_tag() { using source_type = std::remove_cvref_t; if constexpr (requires { typename source_type::value_type; }) { - return std::type_identity{}; + return std::type_identity>{}; } else { - return std::type_identity{}; + return std::type_identity>{}; } } @@ -33,8 +30,8 @@ template constexpr auto to_sample_source(Source& source) { if constexpr (SampleSourceLike) { return source; } else { - using SampleType = sample_type_from_source_t; - return SampleSource{source}; + using StorageType = std::remove_reference_t; + return ReferenceSampleSource{source}; } } @@ -93,8 +90,9 @@ template struct FixedString { template FixedString(const char (&)[N]) -> FixedString; template struct ProtectionSpec { - using source_type = std::remove_cvref_t; + using source_type = std::remove_reference_t; using sample_type = detail::sample_type_from_source_t; + using source_model_type = decltype(detail::to_sample_source(Source)); static constexpr auto name = Name; static constexpr auto& source = Source; @@ -132,7 +130,10 @@ template class ProtectionEngine { template struct StorageForSpec { using spec_type = std::remove_cvref_t; - using type = Protection; + using type = Protection< + typename spec_type::sample_type, + spec_type::rule_count, + typename spec_type::source_model_type>; }; template using storage_for_spec_t = typename StorageForSpec::type; @@ -163,8 +164,9 @@ template class ProtectionEngine { template static constexpr auto make_protection() { using SpecType = std::remove_cvref_t; using SampleType = typename SpecType::sample_type; + using SourceType = typename SpecType::source_model_type; - return Protection{ + return Protection{ SpecType::name.c_str(), detail::to_sample_source(SpecType::source), ProtectionSpec.rules.definitions diff --git a/Inc/ST-LIB_HIGH/Protections/SampleSource.hpp b/Inc/ST-LIB_HIGH/Protections/SampleSource.hpp index c35050a51..0a0e288a6 100644 --- a/Inc/ST-LIB_HIGH/Protections/SampleSource.hpp +++ b/Inc/ST-LIB_HIGH/Protections/SampleSource.hpp @@ -3,16 +3,20 @@ #include "C++Utilities/CppUtils.hpp" #include "ST-LIB_HIGH/Protections/ProtectionConcepts.hpp" -template class SampleSource { +template + requires Protections::ProtectionSample> +class ReferenceSampleSource { public: - using value_type = T; + using value_type = std::remove_cvref_t; - explicit constexpr SampleSource(T& value) : value_ptr(&value) {} - explicit constexpr SampleSource(T* value_ptr) : value_ptr(value_ptr) {} + explicit constexpr ReferenceSampleSource(Storage& value) : value_ptr(&value) {} + explicit constexpr ReferenceSampleSource(Storage* value_ptr) : value_ptr(value_ptr) {} - constexpr const T& read() const { return *value_ptr; } - constexpr T* raw() const { return value_ptr; } + constexpr value_type read() const { return *value_ptr; } + constexpr Storage* raw() const { return value_ptr; } private: - T* value_ptr{nullptr}; + Storage* value_ptr{nullptr}; }; + +template using SampleSource = ReferenceSampleSource; diff --git a/Tests/diagnostics_test.cpp b/Tests/diagnostics_test.cpp index f6b99dfa3..d4573c91b 100644 --- a/Tests/diagnostics_test.cpp +++ b/Tests/diagnostics_test.cpp @@ -22,6 +22,8 @@ namespace TestAccess = ST_LIB::TestAccess; namespace TestPanicReporter = ST_LIB::TestPanicReporter; static_assert(Protections::ReadableSampleSource>); +static_assert(Protections::ReadableSampleSource>); +static_assert(std::same_as::value_type, float>); static_assert(!Protections::ReadableSampleSource); static_assert(Protections::FloatingSample); static_assert(!Protections::FloatingSample); @@ -83,6 +85,14 @@ inline constexpr auto monitored_protection = ); using MonitoredProtectionEngine = Protections::ProtectionEngine; +inline volatile float volatile_monitored_value = 2.0f; +inline constexpr auto volatile_monitored_protection = + Protections::protection<"volatile_monitored_value", volatile_monitored_value>( + Protections::Rules::below(1.0f, 1.5f) + ); +using VolatileMonitoredProtectionEngine = + Protections::ProtectionEngine; + inline float time_value = 0.0f; inline constexpr auto time_protection = Protections::protection<"time_value", time_value>( Protections::Rules::time_accumulation(10.0f, 0.001f) @@ -124,6 +134,7 @@ class DiagnosticsHubTest : public ::testing::Test { void SetUp() override { TestAccess::DiagnosticsHub::clear(); MonitoredProtectionEngine::reset(); + VolatileMonitoredProtectionEngine::reset(); TimeProtectionEngine::reset(); TimeResetProtectionEngine::reset(); TestAccess::FaultController::clear(); @@ -288,6 +299,7 @@ TEST_F(DiagnosticsHubTest, FaultControllerStopsDelegatingAfterFault) { TEST(DiagnosticsBootstrapTest, PanicBeforeRuntimeInstallationSurvivesBootstrapAndIsDelivered) { TestAccess::DiagnosticsHub::clear(); MonitoredProtectionEngine::reset(); + VolatileMonitoredProtectionEngine::reset(); TimeProtectionEngine::reset(); TimeResetProtectionEngine::reset(); TestAccess::FaultController::clear(); @@ -340,6 +352,23 @@ TEST_F(DiagnosticsHubTest, ProtectionEngineEvaluatesRulesAndPublishesSnapshots) EXPECT_EQ(sink->records.front().payload.protection.rule_kind, Protections::RuleKind::BELOW); } +TEST_F(DiagnosticsHubTest, ProtectionEngineReadsVolatileSource) { + auto sink_result = Diagnostics::Hub::emplace_sink(); + ASSERT_TRUE(sink_result.has_value()); + auto* sink = *sink_result; + + VolatileMonitoredProtectionEngine::initialize(); + volatile_monitored_value = 0.5f; + VolatileMonitoredProtectionEngine::evaluate(); + Diagnostics::Hub::flush(); + + ASSERT_FALSE(sink->records.empty()); + EXPECT_TRUE(FaultController::is_faulted()); + EXPECT_EQ(sink->records.front().category, Diagnostics::Category::PROTECTION_EVENT); + EXPECT_EQ(sink->records.front().payload.protection.rule_kind, Protections::RuleKind::BELOW); + EXPECT_FLOAT_EQ(sink->records.front().payload.protection.observed_value.float32_value, 0.5f); +} + TEST_F(DiagnosticsHubTest, TimeAccumulationUsesSchedulerTickForContinuousDuration) { auto sink_result = Diagnostics::Hub::emplace_sink(); ASSERT_TRUE(sink_result.has_value());