diff --git a/core/include/gnuradio-4.0/Block.hpp b/core/include/gnuradio-4.0/Block.hpp index 60404918..def81480 100644 --- a/core/include/gnuradio-4.0/Block.hpp +++ b/core/include/gnuradio-4.0/Block.hpp @@ -198,8 +198,12 @@ inline static const char* kSetting = "Settings"; ///< asynchronous // N.B. 'Set' Settings are first staged before being applied within the work(...) function (real-time/non-real-time decoupling) inline static const char* kStagedSetting = "StagedSettings"; ///< asynchronous message-based staging of settings -inline static const char* kStoreDefaults = "StoreDefaults"; ///< store present settings as default, for counterpart @see kResetDefaults -inline static const char* kResetDefaults = "ResetDefaults"; ///< retrieve and reset to default setting, for counterpart @see kStoreDefaults +inline static const char* kStoreDefaults = "StoreDefaults"; ///< store present settings as default, for counterpart @see kResetDefaults +inline static const char* kResetDefaults = "ResetDefaults"; ///< retrieve and reset to default setting, for counterpart @see kStoreDefaults +inline static const char* kActiveContext = "ActiveContext"; ///< retrieve and set active context +inline static const char* kSettingsCtx = "SettingsCtx"; ///< retrieve/creates/remove a new stored context +inline static const char* kSettingsContexts = "SettingsContexts"; ///< retrieve/creates/remove a new stored context + } // namespace block::property namespace block { @@ -290,6 +294,9 @@ enum class Category { * - `kLifeCycleState`: Manages and reports the block's lifecycle state. * - `kSetting` & `kStagedSetting`: Handle real-time and non-real-time configuration adjustments. * - `kStoreDefaults` & `kResetDefaults`: Facilitate storing and reverting to default settings. + * - `kActiveContext`: Returns current active context and allows to set a new one + * - `kSettingsCtx`: Manages Settings Contexts Add/Remove/Get + * - `kSettingsContexts`: Returns all Contextxs * * These properties can be interacted with through messages, supporting operations like setting values, querying states, and subscribing to updates. * This model provides a flexible interface for blocks to adapt their processing based on runtime conditions and external inputs. @@ -423,13 +430,16 @@ class Block : public lifecycle::StateMachine { using PropertyCallback = std::optional (Derived::*)(std::string_view, Message); std::map propertyCallbacks{ - {block::property::kHeartbeat, &Block::propertyCallbackHeartbeat}, // - {block::property::kEcho, &Block::propertyCallbackEcho}, // - {block::property::kLifeCycleState, &Block::propertyCallbackLifecycleState}, // - {block::property::kSetting, &Block::propertyCallbackSettings}, // - {block::property::kStagedSetting, &Block::propertyCallbackStagedSettings}, // - {block::property::kStoreDefaults, &Block::propertyCallbackStoreDefaults}, // - {block::property::kResetDefaults, &Block::propertyCallbackResetDefaults}, // + {block::property::kHeartbeat, &Block::propertyCallbackHeartbeat}, // + {block::property::kEcho, &Block::propertyCallbackEcho}, // + {block::property::kLifeCycleState, &Block::propertyCallbackLifecycleState}, // + {block::property::kSetting, &Block::propertyCallbackSettings}, // + {block::property::kStagedSetting, &Block::propertyCallbackStagedSettings}, // + {block::property::kStoreDefaults, &Block::propertyCallbackStoreDefaults}, // + {block::property::kResetDefaults, &Block::propertyCallbackResetDefaults}, // + {block::property::kActiveContext, &Block::propertyCallbackActiveContext}, // + {block::property::kSettingsCtx, &Block::propertyCallbackSettingsCtx}, // + {block::property::kSettingsContexts, &Block::propertyCallbackSettingsContexts}, // }; std::map> propertySubscriptions; @@ -1053,6 +1063,162 @@ class Block : public lifecycle::StateMachine { throw gr::exception(fmt::format("block {} property {} does not implement command {}, msg: {}", unique_name, propertyName, message.cmd, message)); } + std::optional propertyCallbackActiveContext(std::string_view propertyName, Message message) { + using enum gr::message::Command; + assert(propertyName == block::property::kActiveContext); + + if (message.cmd == Get) { + message.data = {{"context", settings().activeContext().context}}; + return message; + } + + if (message.cmd == Set) { + if (!message.data.has_value()) { + throw gr::exception(fmt::format("block {} (aka. {}) cannot set {} w/o data msg: {}", unique_name, name, propertyName, message)); + } + + const auto& dataMap = message.data.value(); // Introduced const auto& dataMap + + std::string contextStr; + if (auto it = dataMap.find("context"); it != dataMap.end()) { + if (const auto stringPtr = std::get_if(&it->second); stringPtr) { + contextStr = *stringPtr; + } else { + throw gr::exception(fmt::format("propertyCallbackActiveContext - context is not a string, msg: {}", message)); + } + } else { + throw gr::exception(fmt::format("propertyCallbackActiveContext - context name not found, msg: {}", message)); + } + + std::uint64_t time = 0; + if (auto it = dataMap.find("time"); it != dataMap.end()) { + if (const std::uint64_t* timePtr = std::get_if(&it->second); timePtr) { + time = *timePtr; + } + } + + auto ctx = settings().activateContext(SettingsCtx{ + .time = time, + .context = contextStr, + }); + + if (!ctx.has_value()) { + throw gr::exception(fmt::format("propertyCallbackActiveContext - failed to activate context {}, msg: {}", contextStr, message)); + } + + message.data = {{"context", ctx.value().context}}; + return message; + } + + throw gr::exception(fmt::format("block {} property {} does not implement command {}, msg: {}", unique_name, propertyName, message.cmd, message)); + } + + std::optional propertyCallbackSettingsCtx(std::string_view propertyName, Message message) { + using enum gr::message::Command; + assert(propertyName == block::property::kSettingsCtx); + + if (!message.data.has_value()) { + throw gr::exception(fmt::format("block {} (aka. {}) cannot get/set {} w/o data msg: {}", unique_name, name, propertyName, message)); + } + + const auto& dataMap = message.data.value(); // Introduced const auto& dataMap + + std::string contextStr; + if (auto it = dataMap.find("context"); it != dataMap.end()) { + if (const auto stringPtr = std::get_if(&it->second); stringPtr) { + contextStr = *stringPtr; + } else { + throw gr::exception(fmt::format("propertyCallbackSettingsCtx - context is not a string, msg: {}", message)); + } + } else { + throw gr::exception(fmt::format("propertyCallbackSettingsCtx - context name not found, msg: {}", message)); + } + + std::uint64_t time = 0; + if (auto it = dataMap.find("time"); it != dataMap.end()) { + if (const std::uint64_t* timePtr = std::get_if(&it->second); timePtr) { + time = *timePtr; + } + } + + SettingsCtx ctx{ + .time = time, + .context = contextStr, + }; + + pmtv::map_t parameters; + if (message.cmd == Get) { + std::vector paramKeys; + auto itParam = dataMap.find("parameters"); + if (itParam != dataMap.end()) { + auto keys = std::get_if>(&itParam->second); + if (keys) { + paramKeys = *keys; + } + } + + if (auto params = settings().getStored(paramKeys, ctx); params.has_value()) { + parameters = params.value(); + } + message.data = {{"parameters", parameters}}; + return message; + } + + if (message.cmd == Set) { + if (auto it = dataMap.find("parameters"); it != dataMap.end()) { + auto params = std::get_if(&it->second); + if (params) { + parameters = *params; + } + } + + message.data = {{"failed_to_set", settings().set(parameters, ctx)}}; + return message; + } + + // Removed a Context + if (message.cmd == Disconnect) { + if (ctx.context == "") { + throw gr::exception(fmt::format("propertyCallbackSettingsCtx - cannot delete default context, msg: {}", message)); + } + + if (!settings().removeContext(ctx)) { + throw gr::exception(fmt::format("propertyCallbackSettingsCtx - could not delete context {}, msg: {}", ctx.context, message)); + } + return std::nullopt; + } + + throw gr::exception(fmt::format("block {} property {} does not implement command {}, msg: {}", unique_name, propertyName, message.cmd, message)); + } + + std::optional propertyCallbackSettingsContexts(std::string_view propertyName, Message message) { + using enum gr::message::Command; + assert(propertyName == block::property::kSettingsContexts); + + if (message.cmd == Get) { + const std::map>, settings::PMTCompare>& stored = settings().getStoredAll(); + + std::vector contexts; + std::vector times; + for (const auto& [ctxName, ctxParameters] : stored) { + for (const auto& [ctx, properties] : ctxParameters) { + if (const auto stringPtr = std::get_if(&ctx.context); stringPtr) { + contexts.push_back(*stringPtr); + times.push_back(ctx.time); + } + } + } + + message.data = { + {"contexts", contexts}, + {"times", times}, + }; + return message; + } + + throw gr::exception(fmt::format("block {} property {} does not implement command {}, msg: {}", unique_name, propertyName, message.cmd, message)); + } + protected: /*** * Aggregate the amount of samples that can be consumed/produced from a range of ports. diff --git a/core/include/gnuradio-4.0/Settings.hpp b/core/include/gnuradio-4.0/Settings.hpp index 3a83261a..8cfc8d4d 100644 --- a/core/include/gnuradio-4.0/Settings.hpp +++ b/core/include/gnuradio-4.0/Settings.hpp @@ -191,6 +191,17 @@ struct SettingsBase { virtual void storeDefaults() = 0; virtual void resetDefaults() = 0; + /** + * @brief return the name of the active context + */ + [[nodiscard]] virtual const SettingsCtx& activeContext() const noexcept = 0; + + /** + * @brief removes the given context + * @return true on success + */ + [[nodiscard]] virtual bool removeContext(SettingsCtx ctx) = 0; + /** * @brief Set new activate context and set staged parameters * @return best match context or std::nullopt if best match context is not found in storage @@ -518,6 +529,44 @@ class CtxSettings : public SettingsBase { } } + [[nodiscard]] NO_INLINE const SettingsCtx& activeContext() const noexcept override { return _activeCtx; } + + [[nodiscard]] NO_INLINE bool removeContext(SettingsCtx ctx) override { + if (ctx.context == "") { + return false; // Forbid removing default context + } + + auto it = _storedParameters.find(ctx.context); + if (it == _storedParameters.end()) { + return false; + } + + if (ctx.time == 0ULL) { + ctx.time = settings::convertTimePointToUint64Ns(std::chrono::system_clock::now()); +#ifdef __EMSCRIPTEN__ + ctx.time += _timePrecisionTolerance; +#endif + } + + std::vector>& vec = it->second; + auto exactIt = std::find_if(vec.begin(), vec.end(), [&ctx](const auto& pair) { return pair.first.time == ctx.time; }); + + if (exactIt == vec.end()) { + return false; + } + vec.erase(exactIt); + + if (vec.empty()) { + _storedParameters.erase(ctx.context); + } + + if (_activeCtx.context == ctx.context) { + std::ignore = activateContext(); // Activate default context + } + + return true; + } + [[nodiscard]] NO_INLINE std::optional activateContext(SettingsCtx ctx = {}) override { if (ctx.time == 0ULL) { ctx.time = settings::convertTimePointToUint64Ns(std::chrono::system_clock::now()); diff --git a/core/test/qa_Messages.cpp b/core/test/qa_Messages.cpp index 6042534a..050559fd 100644 --- a/core/test/qa_Messages.cpp +++ b/core/test/qa_Messages.cpp @@ -355,6 +355,177 @@ const boost::ut::suite MessagesTests = [] { expect(eq(43, std::get(stagedSettings.at("factor")))); }; }; + + "Block-level active context tests"_test = [] { + gr::MsgPortOut toBlock; + TestBlock unitTestBlock(property_map{{"name", "UnitTestBlock"}}); + unitTestBlock.init(unitTestBlock.progress, unitTestBlock.ioThreadPool); + gr::MsgPortIn fromBlock; + + expect(eq(ConnectionResult::SUCCESS, toBlock.connect(unitTestBlock.msgIn))); + expect(eq(ConnectionResult::SUCCESS, unitTestBlock.msgOut.connect(fromBlock))); + + "get all contexts default - w/o explicit serviceName"_test = [&] { + sendMessage(toBlock, "" /* serviceName */, block::property::kSettingsContexts /* endpoint */, {} /* data */); + expect(nothrow([&] { unitTestBlock.processScheduledMessages(); })) << "manually execute processing of messages"; + + expect(eq(fromBlock.streamReader().available(), 1UZ)) << "didn't receive reply message"; + const std::map>, settings::PMTCompare> allStored = unitTestBlock.settings().getStoredAll(); + expect(eq(allStored.size(), 1UZ)); + expect(allStored.contains(""s)); + + const Message reply = returnReplyMsg(fromBlock); + expect(reply.cmd == Final) << fmt::format("mismatch between reply.cmd = {} and expected {} command", reply.cmd, Final); + expect(eq(reply.serviceName, unitTestBlock.unique_name)); + expect(eq(reply.clientRequestID, ""s)); + expect(eq(reply.endpoint, std::string(block::property::kSettingsContexts))); + expect(reply.data.has_value()); + expect(reply.data.value().contains("contexts")); + auto contexts = std::get>(reply.data.value().at("contexts")); + expect(eq(contexts.size(), 1UZ)); + expect(eq(contexts, std::vector{""})); + }; + + "get active context - w/o explicit serviceName"_test = [&] { + sendMessage(toBlock, "" /* serviceName */, block::property::kActiveContext /* endpoint */, {} /* data */); + expect(nothrow([&] { unitTestBlock.processScheduledMessages(); })) << "manually execute processing of messages"; + + expect(eq(fromBlock.streamReader().available(), 1UZ)) << "didn't receive reply message"; + const Message reply = returnReplyMsg(fromBlock); + expect(reply.cmd == Final) << fmt::format("mismatch between reply.cmd = {} and expected {} command", reply.cmd, Final); + expect(eq(reply.serviceName, unitTestBlock.unique_name)); + expect(eq(reply.clientRequestID, ""s)); + expect(eq(reply.endpoint, std::string(block::property::kActiveContext))); + expect(reply.data.has_value()); + expect(reply.data.value().contains("context")); + expect(eq(""s, std::get(reply.data.value().at("context")))); + }; + + "create active test_context - w/o explicit serviceName"_test = [&] { + sendMessage(toBlock, "" /* serviceName */, block::property::kSettingsCtx /* endpoint */, {{"context", "test_context"}, {"time", 1UZ}} /* data */); + expect(nothrow([&] { unitTestBlock.processScheduledMessages(); })) << "manually execute processing of messages"; + + expect(eq(fromBlock.streamReader().available(), 1UZ)) << "didn't receive reply message"; + const auto allStored = unitTestBlock.settings().getStoredAll(); + expect(allStored.contains("test_context"s)); + + const Message reply = returnReplyMsg(fromBlock); + expect(reply.cmd == Final) << fmt::format("mismatch between reply.cmd = {} and expected {} command", reply.cmd, Final); + expect(eq(reply.serviceName, unitTestBlock.unique_name)); + expect(eq(reply.clientRequestID, ""s)); + expect(eq(reply.endpoint, std::string(block::property::kSettingsCtx))); + expect(reply.data.has_value()); + expect(reply.data.value().contains("failed_to_set")); + auto failed_to_set = std::get(reply.data.value().at("failed_to_set")); + expect(failed_to_set.empty()); + }; + + "create active new_context - w/o explicit serviceName"_test = [&] { + sendMessage(toBlock, "" /* serviceName */, block::property::kSettingsCtx /* endpoint */, {{"context", "new_context"}, {"time", 2UZ}} /* data */); + expect(nothrow([&] { unitTestBlock.processScheduledMessages(); })) << "manually execute processing of messages"; + + expect(eq(fromBlock.streamReader().available(), 1UZ)) << "didn't receive reply message"; + const auto allStored = unitTestBlock.settings().getStoredAll(); + expect(allStored.contains("new_context"s)); + + const Message reply = returnReplyMsg(fromBlock); + expect(reply.cmd == Final) << fmt::format("mismatch between reply.cmd = {} and expected {} command", reply.cmd, Final); + expect(eq(reply.serviceName, unitTestBlock.unique_name)); + expect(eq(reply.clientRequestID, ""s)); + expect(eq(reply.endpoint, std::string(block::property::kSettingsCtx))); + expect(reply.data.has_value()); + expect(reply.data.value().contains("failed_to_set")); + auto failed_to_set = std::get(reply.data.value().at("failed_to_set")); + expect(failed_to_set.empty()); + }; + + "activate new_context - w/o explicit serviceName"_test = [&] { + sendMessage(toBlock, "" /* serviceName */, block::property::kActiveContext /* endpoint */, {{"context", "new_context"}, {"time", 2UZ}} /* data */); + expect(nothrow([&] { unitTestBlock.processScheduledMessages(); })) << "manually execute processing of messages"; + + expect(eq(fromBlock.streamReader().available(), 1UZ)) << "didn't receive reply message"; + std::string activeContext = std::get(unitTestBlock.settings().activeContext().context); + expect(eq("new_context"s, activeContext)); + + const Message reply = returnReplyMsg(fromBlock); + expect(reply.cmd == Final) << fmt::format("mismatch between reply.cmd = {} and expected {} command", reply.cmd, Final); + expect(eq(reply.serviceName, unitTestBlock.unique_name)); + expect(eq(reply.clientRequestID, ""s)); + expect(eq(reply.endpoint, std::string(block::property::kActiveContext))); + expect(reply.data.has_value()); + expect(reply.data.value().contains("context")); + expect(eq("new_context"s, std::get(reply.data.value().at("context")))); + }; + + "get active new_context - w/o explicit serviceName"_test = [&] { + sendMessage(toBlock, "" /* serviceName */, block::property::kActiveContext /* endpoint */, {} /* data */); + expect(nothrow([&] { unitTestBlock.processScheduledMessages(); })) << "manually execute processing of messages"; + + expect(eq(fromBlock.streamReader().available(), 1UZ)) << "didn't receive reply message"; + const Message reply = returnReplyMsg(fromBlock); + expect(reply.cmd == Final) << fmt::format("mismatch between reply.cmd = {} and expected {} command", reply.cmd, Final); + expect(eq(reply.serviceName, unitTestBlock.unique_name)); + expect(eq(reply.clientRequestID, ""s)); + expect(eq(reply.endpoint, std::string(block::property::kActiveContext))); + expect(reply.data.has_value()); + expect(reply.data.value().contains("context")); + expect(eq("new_context"s, std::get(reply.data.value().at("context")))); + }; + + "get all contexts - w/o explicit serviceName"_test = [&] { + expect(eq(unitTestBlock.settings().getStoredAll().size(), 3UZ)); + + sendMessage(toBlock, "" /* serviceName */, block::property::kSettingsContexts /* endpoint */, {} /* data */); + expect(nothrow([&] { unitTestBlock.processScheduledMessages(); })) << "manually execute processing of messages"; + + expect(eq(fromBlock.streamReader().available(), 1UZ)) << "didn't receive reply message"; + const std::map>, settings::PMTCompare> allStored = unitTestBlock.settings().getStoredAll(); + expect(eq(allStored.size(), 3UZ)); + expect(allStored.contains(""s)); + expect(allStored.contains("new_context"s)); + expect(allStored.contains("test_context"s)); + + const Message reply = returnReplyMsg(fromBlock); + expect(reply.cmd == Final) << fmt::format("mismatch between reply.cmd = {} and expected {} command", reply.cmd, Final); + expect(eq(reply.serviceName, unitTestBlock.unique_name)); + expect(eq(reply.clientRequestID, ""s)); + expect(eq(reply.endpoint, std::string(block::property::kSettingsContexts))); + expect(reply.data.has_value()); + expect(reply.data.value().contains("contexts")); + auto contexts = std::get>(reply.data.value().at("contexts")); + auto times = std::get>(reply.data.value().at("times")); + expect(eq(contexts.size(), 3UZ)); + expect(eq(times.size(), 3UZ)); + expect(eq(contexts, std::vector{"", "new_context", "test_context"})); + // We do not check the default context as it's time is now() + expect(eq(times[1], 2UZ)); + expect(eq(times[2], 1UZ)); + }; + + "remove new_context - w/o explicit serviceName"_test = [&] { + sendMessage(toBlock, "" /* serviceName */, block::property::kSettingsCtx /* endpoint */, {{"context", "new_context"}, {"time", 2UZ}} /* data */); + expect(nothrow([&] { unitTestBlock.processScheduledMessages(); })) << "manually execute processing of messages"; + + expect(eq(fromBlock.streamReader().available(), 0UZ)) << "should not receive a reply"; + std::string activeContext = std::get(unitTestBlock.settings().activeContext().context); + expect(eq(""s, activeContext)); + }; + + "get active back to default context '' - w/o explicit serviceName"_test = [&] { + sendMessage(toBlock, "" /* serviceName */, block::property::kActiveContext /* endpoint */, {} /* data */); + expect(nothrow([&] { unitTestBlock.processScheduledMessages(); })) << "manually execute processing of messages"; + + expect(eq(fromBlock.streamReader().available(), 1UZ)) << "didn't receive reply message"; + const Message reply = returnReplyMsg(fromBlock); + expect(reply.cmd == Final) << fmt::format("mismatch between reply.cmd = {} and expected {} command", reply.cmd, Final); + expect(eq(reply.serviceName, unitTestBlock.unique_name)); + expect(eq(reply.clientRequestID, ""s)); + expect(eq(reply.endpoint, std::string(block::property::kActiveContext))); + expect(reply.data.has_value()); + expect(reply.data.value().contains("context")); + expect(eq(""s, std::get(reply.data.value().at("context")))); + }; + }; }; "Multi-Block message passing tests"_test = [] {