Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/CI.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ on:

jobs:
integration-test-in-studio-container:
uses: PickNikRobotics/moveit_pro_ci/.github/workflows/workspace_integration_test.yaml@v0.0.6
uses: PickNikRobotics/moveit_pro_ci/.github/workflows/workspace_integration_test.yaml@v0.0.7
with:
runner: "ubuntu-22.04"
image_tag: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || github.ref_name }}
Expand Down
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ add_library(
src/access_interface_value_from_group.cpp
src/get_pose_stamped_from_topic.cpp
src/publish_dynamic_interface_group_values.cpp
src/get_blackboard_by_key.cpp
src/set_blackboard_by_key.cpp
src/register_behaviors.cpp)
target_include_directories(
experimental_behaviors
Expand Down
48 changes: 48 additions & 0 deletions include/experimental_behaviors/get_blackboard_by_key.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright 2026 PickNik Inc.
// All rights reserved.
//
// Unauthorized copying of this code base via any medium is strictly prohibited.
// Proprietary and confidential.

#pragma once

#include <behaviortree_cpp/action_node.h>
#include <moveit_pro_behavior_interface/behavior_context.hpp>
#include <moveit_pro_behavior_interface/shared_resources_node.hpp>

namespace experimental_behaviors
{
/**
* @brief Reads a blackboard entry whose key is determined dynamically at tick
* time from an input port, and copies its value to an output port.
*
* @details
* This is the read-side complement of BT.CPP's native SetBlackboard, which
* already supports a dynamic `output_key` port but has no corresponding
* read-by-dynamic-key primitive.
*
* | Data Port Name | Port Type | Object Type |
* | -------------- | --------- | -------------------- |
* | key | input | std::string |
* | value | output | any (AnyTypeAllowed) |
*
* The `key` port accepts both literal strings and root-scope references
* prefixed with `@`. Returns FAILURE if the key is missing or empty.
*
* An entry that exists but holds an empty value is also treated as a cache
* miss and reported as FAILURE. Callers that need to distinguish "declared
* but uninitialized" from "never declared" should populate the entry with
* an explicit sentinel before reading.
*/
class GetBlackboardByKey final : public moveit_pro::behaviors::SharedResourcesNode<BT::SyncActionNode>
{
public:
GetBlackboardByKey(const std::string& name, const BT::NodeConfiguration& config,
const std::shared_ptr<moveit_pro::behaviors::BehaviorContext>& shared_resources);

static BT::PortsList providedPorts();
static BT::KeyValueVector metadata();

BT::NodeStatus tick() override;
};
} // namespace experimental_behaviors
55 changes: 55 additions & 0 deletions include/experimental_behaviors/set_blackboard_by_key.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2026 PickNik Inc.
// All rights reserved.
//
// Unauthorized copying of this code base via any medium is strictly prohibited.
// Proprietary and confidential.

#pragma once

#include <behaviortree_cpp/action_node.h>
#include <moveit_pro_behavior_interface/behavior_context.hpp>
#include <moveit_pro_behavior_interface/shared_resources_node.hpp>

namespace experimental_behaviors
{
/**
* @brief Writes the value of an input port to a blackboard entry whose key is
* determined dynamically at tick time.
*
* @details
* Write-side complement to GetBlackboardByKey and a drop-in replacement for
* BT.CPP's native SetBlackboard when the source value is carried on a port
* typed as `AnyTypeAllowed` (e.g. a value produced by a Script `:=`
* assignment).
*
* BT.CPP's SetBlackboard runs a string-conversion step whenever the
* destination entry is not strictly `std::string` and the source value is a
* string. For `AnyTypeAllowed` destinations there is no registered converter,
* so the conversion produces an empty Any and silently clobbers the
* destination — see set_blackboard_node.h and basic_types.cpp::parseString.
*
* This behavior copies the source Any directly, preserving both type and
* value, which matches the semantics callers expect when caching arbitrary
* subtree outputs under runtime-computed keys.
*
* | Data Port Name | Port Type | Object Type |
* | -------------- | --------- | -------------------- |
* | key | input | std::string |
* | value | input | any (AnyTypeAllowed) |
*
* The `key` port accepts both literal strings and root-scope references
* prefixed with `@`. Returns FAILURE if `key` is missing/empty or if the
* `value` port refers to a blackboard entry that does not exist.
*/
class SetBlackboardByKey final : public moveit_pro::behaviors::SharedResourcesNode<BT::SyncActionNode>
{
public:
SetBlackboardByKey(const std::string& name, const BT::NodeConfiguration& config,
const std::shared_ptr<moveit_pro::behaviors::BehaviorContext>& shared_resources);

static BT::PortsList providedPorts();
static BT::KeyValueVector metadata();

BT::NodeStatus tick() override;
};
} // namespace experimental_behaviors
87 changes: 87 additions & 0 deletions src/get_blackboard_by_key.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright 2026 PickNik Inc.
// All rights reserved.
//
// Unauthorized copying of this code base via any medium is strictly prohibited.
// Proprietary and confidential.

#include <experimental_behaviors/get_blackboard_by_key.hpp>

#include <moveit_pro_behavior_interface/metadata_fields.hpp>

namespace
{
inline constexpr auto kDescriptionGetBlackboardByKey = R"(
<p>
Reads a blackboard entry whose key is computed at runtime (typically via a Script node
building a string from other blackboard variables) and copies the entry's value to an
output port. The key may be prefixed with '@' to address the root blackboard.
</p>
<p>
Returns FAILURE if the key refers to a missing blackboard entry. This is the read-side
complement to BT.CPP's native SetBlackboard.
Comment thread
WillYingling marked this conversation as resolved.
Outdated
</p>
)";

constexpr auto kPortIDKey = "key";
constexpr auto kPortIDValue = "value";
} // namespace

namespace experimental_behaviors
{
GetBlackboardByKey::GetBlackboardByKey(const std::string& name, const BT::NodeConfiguration& config,
const std::shared_ptr<moveit_pro::behaviors::BehaviorContext>& shared_resources)
: moveit_pro::behaviors::SharedResourcesNode<BT::SyncActionNode>(name, config, shared_resources)
{
}

BT::PortsList GetBlackboardByKey::providedPorts()
{
return { BT::InputPort<std::string>(kPortIDKey,
"Blackboard key to read from. May be constructed dynamically via Script. "
"Prefix with '@' for root-scope access."),
BT::OutputPort(kPortIDValue, "Value read from the blackboard entry referenced by `key`.") };
}

BT::KeyValueVector GetBlackboardByKey::metadata()
{
return { { moveit_pro::behaviors::kSubcategoryMetadataKey, "Blackboard" },
{ moveit_pro::behaviors::kDescriptionMetadataKey, kDescriptionGetBlackboardByKey } };
}

BT::NodeStatus GetBlackboardByKey::tick()
{
std::string key;
if (!getInput<std::string>(kPortIDKey, key) || key.empty())
{
shared_resources_->logger->publishFailureMessage(name(), "Missing or empty input port [key]");
return BT::NodeStatus::FAILURE;
}

auto src_entry = config().blackboard->getEntry(key);
if (!src_entry)
{
shared_resources_->logger->publishFailureMessage(name(),
"Blackboard entry '" + key + "' does not exist (cache miss).");
return BT::NodeStatus::FAILURE;
}

if (src_entry->value.empty())
{
shared_resources_->logger->publishFailureMessage(name(),
"Blackboard entry '" + key + "' exists but holds no value.");
return BT::NodeStatus::FAILURE;
}

// Pass the stored BT::Any through the canonical setOutput path so SubTree
// auto-remapping and blackboard-pointer validation are handled consistently
// with the rest of BT.CPP (see tree_node.h setOutput).
const auto result = setOutput<BT::Any>(kPortIDValue, src_entry->value);
if (!result)
{
shared_resources_->logger->publishFailureMessage(name(), result.error());
return BT::NodeStatus::FAILURE;
}

return BT::NodeStatus::SUCCESS;
}
} // namespace experimental_behaviors
4 changes: 4 additions & 0 deletions src/register_behaviors.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
#include "experimental_behaviors/access_interface_value_from_group.hpp"
#include "experimental_behaviors/create_dynamic_interface_group_values.hpp"
#include "experimental_behaviors/create_interface_value.hpp"
#include "experimental_behaviors/get_blackboard_by_key.hpp"
#include "experimental_behaviors/get_dynamic_interface_group_values.hpp"
#include "experimental_behaviors/get_interface_value_from_group.hpp"
#include "experimental_behaviors/get_pose_stamped_from_topic.hpp"
#include "experimental_behaviors/publish_dynamic_interface_group_values.hpp"
#include "experimental_behaviors/set_blackboard_by_key.hpp"

#include <pluginlib/class_list_macros.hpp>

Expand All @@ -40,6 +42,8 @@ class ExperimentalBehaviorsLoader : public moveit_pro::behaviors::SharedResource
shared_resources);
moveit_pro::behaviors::registerBehavior<AccessInterfaceValueFromGroup>(factory, "AccessInterfaceValue",
shared_resources);
moveit_pro::behaviors::registerBehavior<GetBlackboardByKey>(factory, "GetBlackboardByKey", shared_resources);
moveit_pro::behaviors::registerBehavior<SetBlackboardByKey>(factory, "SetBlackboardByKey", shared_resources);
}
};
} // namespace experimental_behaviors
Expand Down
122 changes: 122 additions & 0 deletions src/set_blackboard_by_key.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright 2026 PickNik Inc.
// All rights reserved.
//
// Unauthorized copying of this code base via any medium is strictly prohibited.
// Proprietary and confidential.

#include <experimental_behaviors/set_blackboard_by_key.hpp>

#include <moveit_pro_behavior_interface/metadata_fields.hpp>

namespace
{
inline constexpr auto kDescriptionSetBlackboardByKey = R"(
<p>
Writes a source port's value to a blackboard entry whose key is computed at runtime
(typically via a Script node building a string from other blackboard variables).
The key may be prefixed with '@' to address the root blackboard.
</p>
<p>
Use this instead of BT.CPP's native SetBlackboard when the source is an Any-typed
port (e.g. produced by a Script `:=` assignment): SetBlackboard runs a
string-to-destination-type conversion that silently empties the value when the
destination type is AnyTypeAllowed and no converter is registered. This behavior
copies the source Any directly.
</p>
)";

constexpr auto kPortIDKey = "key";
constexpr auto kPortIDValue = "value";
} // namespace

namespace experimental_behaviors
{
SetBlackboardByKey::SetBlackboardByKey(const std::string& name, const BT::NodeConfiguration& config,
const std::shared_ptr<moveit_pro::behaviors::BehaviorContext>& shared_resources)
: moveit_pro::behaviors::SharedResourcesNode<BT::SyncActionNode>(name, config, shared_resources)
{
}

BT::PortsList SetBlackboardByKey::providedPorts()
{
return { BT::InputPort<std::string>(kPortIDKey,
"Blackboard key to write to. May be constructed dynamically via Script. "
"Prefix with '@' for root-scope access."),
BT::InputPort(kPortIDValue, "Source value. Accepts a blackboard pointer ({some_port}) whose "
"Any contents are copied verbatim, or a literal string.") };
}

BT::KeyValueVector SetBlackboardByKey::metadata()
{
return { { moveit_pro::behaviors::kSubcategoryMetadataKey, "Blackboard" },
{ moveit_pro::behaviors::kDescriptionMetadataKey, kDescriptionSetBlackboardByKey } };
}

BT::NodeStatus SetBlackboardByKey::tick()
{
std::string key;
if (!getInput<std::string>(kPortIDKey, key) || key.empty())
{
shared_resources_->logger->publishFailureMessage(name(), "Missing or empty input port [key]");
return BT::NodeStatus::FAILURE;
}

// Read the raw port expression from config rather than going through
// getInput<std::string>, which would stringify whatever the source port
// holds. We need the untouched Any so trajectories, vectors, etc. survive
// the copy.
const auto value_it = config().input_ports.find(kPortIDValue);
if (value_it == config().input_ports.end())
{
shared_resources_->logger->publishFailureMessage(name(), "Missing input port [value]");
return BT::NodeStatus::FAILURE;
}
const std::string& value_expr = value_it->second;

BT::StringView stripped_key;
BT::Any out_value;
BT::TypeInfo src_info;
if (BT::TreeNode::isBlackboardPointer(value_expr, &stripped_key))
{
const auto input_key = std::string(stripped_key);
auto src_entry = config().blackboard->getEntry(input_key);
if (!src_entry)
{
shared_resources_->logger->publishFailureMessage(
name(), "Source port '" + input_key + "' for [value] does not reference an existing blackboard entry.");
return BT::NodeStatus::FAILURE;
}
out_value = src_entry->value;
src_info = src_entry->info;
}
else
{
out_value = BT::Any(value_expr);
src_info = BT::TypeInfo::Create<std::string>();
}

// Ensure the destination entry exists before writing. Create it with the
// source entry's TypeInfo so downstream consumers that inspect entry->info
// see a consistent type (e.g. AnyTypeAllowed vs std::string vs trajectory).
auto dst_entry = config().blackboard->getEntry(key);
if (!dst_entry)
{
config().blackboard->createEntry(key, src_info);
dst_entry = config().blackboard->getEntry(key);
if (!dst_entry)
{
shared_resources_->logger->publishFailureMessage(name(), "Failed to create blackboard entry '" + key + "'");
return BT::NodeStatus::FAILURE;
}
}

{
std::scoped_lock lock(dst_entry->entry_mutex);
dst_entry->value = out_value;
dst_entry->sequence_id++;
dst_entry->stamp = std::chrono::steady_clock::now().time_since_epoch();
}

return BT::NodeStatus::SUCCESS;
}
} // namespace experimental_behaviors
2 changes: 2 additions & 0 deletions test/test_behavior_plugins.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ TEST(BehaviorTests, test_load_behavior_plugins)
// Test that ClassLoader is able to find and instantiate each behavior using the package's plugin description info.
EXPECT_NO_THROW((void)factory.instantiateTreeNode("test_get_pose_stamped_from_topic", "GetPoseStampedFromTopic",
BT::NodeConfiguration()));
EXPECT_NO_THROW(
(void)factory.instantiateTreeNode("test_get_blackboard_by_key", "GetBlackboardByKey", BT::NodeConfiguration()));
}

int main(int argc, char** argv)
Expand Down
Loading