Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
88 changes: 88 additions & 0 deletions src/get_blackboard_by_key.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// 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 input <code>key</code> port is missing or empty, if the key
refers to a blackboard entry that does not exist, or if the referenced entry exists
but holds no value. This is the read-side complement to BT.CPP's native SetBlackboard.
</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