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
37 changes: 37 additions & 0 deletions include/behaviortree_cpp/basic_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,43 @@ using KeyValueVector = std::vector<std::pair<std::string, std::string>>;
template <typename T>
using Expected = nonstd::expected<T, std::string>;

/**
* @brief Machine-readable cause for why an input port could not be read.
*
* The plain string error returned by TreeNode::getInputStamped() is fine for
* console messages, but callers that want to react differently to different
* failure modes (for example, "the user wired a blackboard key but forgot to
* populate it" versus "the port is simply unwired and the behavior should
* fall back to its default") cannot tell those cases apart from a string.
*
* TreeNode::getInputStampedWithDiagnostic() returns this enum alongside the
* original human-readable message so upstream code can make that distinction.
*/
enum class PortError : uint8_t
{
ManifestMissing, ///< config().manifest is null and the port has no XML entry.
ManifestKeyMissing, ///< The key is not in the manifest and not in the XML.
NoDefaultNoWiring, ///< The manifest has the key but no default value and the XML did not wire it.
BlackboardKeyNotFound, ///< The port is wired to {foo} but the blackboard has no entry called foo.
BlackboardEntryEmpty, ///< The blackboard entry exists but its value is empty.
InvalidBlackboard, ///< config().blackboard is null.
ConversionFailed, ///< parseString<T>() threw while converting a string to T.
CastFailed, ///< Any::cast<T>() or a vector element cast threw.
};

/**
* @brief Diagnostic error returned by TreeNode::getInputStampedWithDiagnostic().
*
* `code` carries the machine-readable cause; `message` preserves the same
* human-readable text the non-diagnostic getInputStamped() overload would
* have returned so existing log output is unchanged.
*/
struct PortInputError
{
PortError code;
std::string message;
};

struct AnyTypeAllowed
{
};
Expand Down
120 changes: 96 additions & 24 deletions include/behaviortree_cpp/tree_node.h
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,20 @@ class TreeNode
[[nodiscard]] Expected<Timestamp> getInputStamped(const std::string& key,
T& destination) const;

/**
* @brief Same as getInputStamped(key, destination) but returns a structured
* PortInputError on failure, so callers can distinguish the different reasons
* a port read can fail (unwired manifest key vs. blackboard key missing vs.
* conversion failure, etc.). The `.message` field preserves the exact
* human-readable text getInputStamped() would have returned.
*
* @param key the name of the port.
* @param destination reference to the object where the value should be stored
*/
template <typename T>
[[nodiscard]] nonstd::expected<Timestamp, PortInputError>
getInputStampedWithDiagnostic(const std::string& key, T& destination) const;

/** Same as bool getInput(const std::string& key, T& destination)
* but using optional.
*
Expand Down Expand Up @@ -296,6 +310,28 @@ class TreeNode
}
}

/** Value-returning convenience for getInputStampedWithDiagnostic().
*
* Returns the port value directly on success, or a nonstd::expected carrying
* the structured PortInputError on failure. Use this when you only need the
* value (not the Timestamp) but still want to distinguish the failure
* reasons the diagnostic variant exposes.
*
* @param key the name of the port.
*/
template <typename T>
[[nodiscard]] nonstd::expected<T, PortInputError>
getInputWithDiagnostic(const std::string& key) const
{
T out{};
auto res = getInputStampedWithDiagnostic(key, out);
if(res)
{
return out;
}
return nonstd::make_unexpected(res.error());
}

/**
* @brief setOutput modifies the content of an Output port
* @param key the name of the port.
Expand Down Expand Up @@ -450,8 +486,8 @@ T TreeNode::parseString(const std::string& str) const
}

template <typename T>
inline Expected<Timestamp> TreeNode::getInputStamped(const std::string& key,
T& destination) const
inline nonstd::expected<Timestamp, PortInputError>
TreeNode::getInputStampedWithDiagnostic(const std::string& key, T& destination) const
{
std::string port_value_str;

Expand All @@ -462,30 +498,33 @@ inline Expected<Timestamp> TreeNode::getInputStamped(const std::string& key,
}
else if(!config().manifest)
{
return nonstd::make_unexpected(StrCat("getInput() of node '", fullPath(),
"' failed because the manifest is "
"nullptr (WTF?) and the key: [",
key, "] is missing"));
return nonstd::make_unexpected(PortInputError{
PortError::ManifestMissing, StrCat("getInput() of node '", fullPath(),
"' failed because the manifest is "
"nullptr (WTF?) and the key: [",
key, "] is missing") });
}
else
{
// maybe it is declared with a default value in the manifest
auto port_manifest_it = config().manifest->ports.find(key);
if(port_manifest_it == config().manifest->ports.end())
{
return nonstd::make_unexpected(StrCat("getInput() of node '", fullPath(),
"' failed because the manifest doesn't "
"contain the key: [",
key, "]"));
return nonstd::make_unexpected(PortInputError{
PortError::ManifestKeyMissing, StrCat("getInput() of node '", fullPath(),
"' failed because the manifest doesn't "
"contain the key: [",
key, "]") });
}
const auto& port_info = port_manifest_it->second;
// there is a default value
if(port_info.defaultValue().empty())
{
return nonstd::make_unexpected(StrCat("getInput() of node '", fullPath(),
"' failed because nor the manifest or the "
"XML contain the key: [",
key, "]"));
return nonstd::make_unexpected(PortInputError{
PortError::NoDefaultNoWiring, StrCat("getInput() of node '", fullPath(),
"' failed because nor the manifest or the "
"XML contain the key: [",
key, "]") });
}
if(port_info.defaultValue().isString())
{
Expand All @@ -510,16 +549,20 @@ inline Expected<Timestamp> TreeNode::getInputStamped(const std::string& key,
}
catch(std::exception& ex)
{
return nonstd::make_unexpected(StrCat("getInput(): ", ex.what()));
return nonstd::make_unexpected(PortInputError{
PortError::ConversionFailed, StrCat("getInput(): ", ex.what()) });
}
return Timestamp{};
}
const auto& blackboard_key = blackboard_ptr.value();

if(!config().blackboard)
{
return nonstd::make_unexpected("getInput(): trying to access "
"an invalid Blackboard");
// clang-format off
return nonstd::make_unexpected(PortInputError{
PortError::InvalidBlackboard,
"getInput(): trying to access an invalid Blackboard" });
// clang-format on
}

if(auto entry = config().blackboard->getEntry(std::string(blackboard_key)))
Expand Down Expand Up @@ -549,9 +592,13 @@ inline Expected<Timestamp> TreeNode::getInputStamped(const std::string& key,
if(!any_vec.empty() &&
any_vec.front().type() != typeid(typename T::value_type))
{
return nonstd::make_unexpected("Invalid cast requested from vector<Any> to "
"vector<typename T::value_type>."
" Element type does not align.");
return nonstd::make_unexpected(PortInputError{ PortError::CastFailed,
"Invalid cast requested "
"from vector<Any> to "
"vector<typename "
"T::value_type>."
" Element type does not "
"align." });
}
destination = T();
std::transform(
Expand All @@ -570,16 +617,41 @@ inline Expected<Timestamp> TreeNode::getInputStamped(const std::string& key,
}
return Timestamp{ entry->sequence_id, entry->stamp };
}

// The entry exists on the blackboard but its value is empty (for example,
// it was created via createEntry() without ever being set).
return nonstd::make_unexpected(
PortInputError{ PortError::BlackboardEntryEmpty,
StrCat("getInput() failed because it was unable to "
"find the key [",
key, "] remapped to [", blackboard_key, "]") });
}

return nonstd::make_unexpected(StrCat("getInput() failed because it was unable to "
"find the key [",
key, "] remapped to [", blackboard_key, "]"));
// No entry on the blackboard at all: the port was wired to a key that was
// never populated. This is the case that callers most often want to surface
// as a warning, because it usually indicates a wiring mistake by the user.
return nonstd::make_unexpected(
PortInputError{ PortError::BlackboardKeyNotFound,
StrCat("getInput() failed because it was unable to "
"find the key [",
key, "] remapped to [", blackboard_key, "]") });
}
catch(std::exception& err)
{
return nonstd::make_unexpected(err.what());
return nonstd::make_unexpected(PortInputError{ PortError::CastFailed, err.what() });
}
}

template <typename T>
inline Expected<Timestamp> TreeNode::getInputStamped(const std::string& key,
T& destination) const
{
auto res = getInputStampedWithDiagnostic<T>(key, destination);
if(res)
{
return res.value();
}
return nonstd::make_unexpected(std::move(res.error().message));
}

template <typename T>
Expand Down
Loading
Loading