Program Listing for File parameter.h
↰ Return to documentation for file (include/hatchbed_common/parameter.h)
#pragma once
#include <functional>
#include <memory>
#include <mutex>
#include <sstream>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include <rclcpp/rclcpp.hpp>
#include <rcutils/logging.h>
namespace hatchbed_common {
template <class T>
struct OwnedStore {
using Ptr = std::shared_ptr<OwnedStore>;
OwnedStore(const T& value) : value(value) {}
T value;
std::mutex mutex;
};
template <class T>
struct BorrowedStore {
using Ptr = std::shared_ptr<BorrowedStore>;
BorrowedStore(T* value) : value(value) {}
T* value;
std::mutex mutex;
};
template <class T>
struct EnumOption {
T value;
std::string name;
std::string description;
};
class ParamHandler;
template <typename T>
struct value_type_of {
using type = T; // Default: scalar types stay unchanged
};
template <typename T, typename U>
struct value_type_of<std::vector<T, U>> {
using type = T; // Specialization for std::vector
};
template <class T>
class Parameter {
public:
using U = typename value_type_of<T>::type;
class Declared {
public:
Declared(const Parameter& param) : param_(param) {}
T value() { return param_.value(); }
bool update(const T& value) { return param_.update(value, false); }
private:
Parameter param_;
};
Parameter(T* store, const std::string& ns, const std::string& name, T default_val, const std::string& description,
std::shared_ptr<rclcpp::Node> node)
: namespace_(ns),
name_(name),
default_val_(default_val),
description_(description),
node_(node)
{
if (!store) {
owned_store_ = std::make_shared<OwnedStore<T>>(default_val_);
}
else {
borrowed_store_ = std::make_shared<BorrowedStore<T>>(store);
*(borrowed_store_->value) = default_val_;
}
}
Parameter() = default;
Parameter(const Parameter& parameter) = default;
virtual ~Parameter() = default;
virtual Declared declare() {
registerParam();
return Declared(*this);
}
// NOTE: This class uses method chaining (i.e. modifiers to the parameter return the object reference so that you can chain the modifiers together)
// e.g. params.param("param_name", 0).min(0).max(10).step(1).callback([](int value) { ... }).declare();
// Any child class that creates a new method should override any parent class chaining methods to return the child class type.
// e.g. class ChildParameter : public Parameter<int> {
// virtual ChildParameter& dynamic() override {
// ParameterBase<int>::dynamic();
// return *this;
// }
// };
// Otherwise the return value will be of type ParameterBase<T> and not the child class type and any child class methods will not be available.
virtual Parameter<T>& callback(std::function<void(T)> callback) {
is_dynamic_ = true;
user_callback_ = callback;
return *this;
}
virtual Parameter<T>& dynamic() {
is_dynamic_ = true;
return *this;
}
T defaultValue() const {
return default_val_;
}
std::string qualifiedName() const {
if (!is_dynamic_) {
return name_ + " (readonly)";
}
return name_;
}
std::string description() const {
return description_;
}
bool isDynamic() const {
return is_dynamic_;
}
protected:
T value() const {
if (owned_store_) {
std::scoped_lock lock(owned_store_->mutex);
return owned_store_->value;
}
std::scoped_lock lock(borrowed_store_->mutex);
return *borrowed_store_->value;
}
virtual bool update(const T& value, bool from_callback = false) {
if (initialized_ && !is_dynamic_) {
RCLCPP_WARN_STREAM(node_->get_logger(), namespace_ << "/" << name_ << " is static and can't be updated.");
return false;
}
bool did_change = false;
if (owned_store_) {
std::scoped_lock lock(owned_store_->mutex);
did_change = owned_store_->value != value;
if (did_change && from_callback) {
RCLCPP_INFO_STREAM(node_->get_logger(), "updated <" << namespace_ << "/" << name_ << ">: "
<< toString(owned_store_->value) << " to " << toString(value));
}
owned_store_->value = value;
}
else {
std::scoped_lock lock(borrowed_store_->mutex);
did_change = *borrowed_store_->value != value;
if (did_change && from_callback) {
RCLCPP_INFO_STREAM(node_->get_logger(), "updated <" << namespace_ << "/" << name_ << ">: "
<< toString(*borrowed_store_->value) <<" to " << toString(value));
}
*borrowed_store_->value = value;
}
if (!from_callback && did_change) {
node_->set_parameter({ name_, rclcpp::ParameterValue(this->value()) });
}
if (user_callback_ && did_change) {
user_callback_(value);
}
return true;
}
// overloaded base type
virtual std::string toString(const U& value) const {
if constexpr (std::is_same_v<U, bool>) {
// bool is a special common case
// other special cases are handled in the child classes
return value ? "true" : "false";
} else {
// general case ss works for all basic types
std::stringstream ss;
ss << value;
return ss.str();
}
}
// overloaded for vector
virtual std::string toString(const std::vector<U>& values) const {
std::stringstream ss;
ss << "[";
for (size_t i = 0; i < values.size(); ++i) {
ss << toString(values[i]);
if (i != values.size() - 1) {
ss << ", ";
}
}
ss << "]";
return ss.str();
}
virtual void registerParam() {
rcl_interfaces::msg::ParameterDescriptor descriptor;
descriptor.description = description_;
descriptor.read_only = !is_dynamic_;
try {
node_->declare_parameter(name_, rclcpp::ParameterValue(default_val_), descriptor);
}
catch (const rclcpp::exceptions::ParameterAlreadyDeclaredException& e) {
RCLCPP_WARN(node_->get_logger(), "Parameter [%s] already declared: %s", name_.c_str(),
e.what());
}
update(getParameter());
RCLCPP_INFO_STREAM(node_->get_logger(), namespace_ << "/" << name_ << ": " << toString(value()));
initialized_ = true;
}
T getParameter() {
auto p = node_->get_parameter(name_);
return p.get_value<T>();
}
std::string namespace_;
std::string name_;
T default_val_;
std::string description_;
std::shared_ptr<rclcpp::Node> node_;
typename BorrowedStore<T>::Ptr borrowed_store_;
typename OwnedStore<T>::Ptr owned_store_;
bool initialized_ = false;
bool is_dynamic_ = false;
std::function<void(T)> user_callback_;
friend class ParamHandler;
};
template <typename T>
class NumericParameter : public Parameter<T> {
using U = typename value_type_of<T>::type;
public:
NumericParameter(T* store, const std::string& ns, const std::string& name, T default_val,
const std::string& description, std::shared_ptr<rclcpp::Node> node)
: Parameter<T>(store, ns, name, default_val, description, node) {}
NumericParameter() = default;
NumericParameter(const NumericParameter& parameter) = default;
virtual ~NumericParameter() = default;
virtual NumericParameter<T>& callback(std::function<void(T)> callback) override {
Parameter<T>::callback(callback);
return *this;
}
virtual NumericParameter<T>& dynamic() override {
Parameter<T>::dynamic();
return *this;
}
virtual NumericParameter<T>& min(U min) {
min_ = min;
has_range_ = true;
if (max_ < min_) {
max_ = min_;
}
this->default_val_ = clamp(this->default_val_);
return *this;
}
virtual NumericParameter<T>& max(U max) {
max_ = max;
has_range_ = true;
if (min_ > max_) {
min_ = max_;
}
this->default_val_ = clamp(this->default_val_);
return *this;
}
virtual NumericParameter<T>& step(U step) {
step_ = step;
return *this;
}
U min() const {
return min_;
}
bool hasRange() const {
return has_range_;
}
U max() const {
return max_;
}
U clamp(const U& value) const {
U clamped = value;
if (has_range_ && clamped < min_) {
clamped = min_;
}
if (has_range_ && clamped > max_) {
clamped = max_;
}
return clamped;
}
std::vector<U> clamp(const std::vector<U>& values) const {
std::vector<U> clamped;
for (const auto& value : values) {
clamped.push_back(clamp(value));
}
return clamped;
}
protected:
bool checkRange(const U& value) const {
if (has_range_ && (value < min_ || value > max_)) {
RCLCPP_WARN_STREAM(this->node_->get_logger(), this->namespace_ << "/" << this->name_
<< ": value out of range <" << value << ">");
return false;
}
return true;
}
bool checkRange(const std::vector<U>& values) const {
for (const auto& value : values) {
if (!checkRange(value)) {
return false;
}
}
return true;
}
virtual bool update(const T& value, bool from_callback = false) {
if (!checkRange(value)) {
return false;
}
return Parameter<T>::update(value, from_callback);
}
virtual void registerParam() override {
rcl_interfaces::msg::ParameterDescriptor descriptor;
descriptor.description = this->description_;
descriptor.read_only = !this->is_dynamic_;
if (has_range_) {
descriptor.floating_point_range.resize(1);
descriptor.floating_point_range[0].from_value = min_;
descriptor.floating_point_range[0].to_value = max_;
descriptor.floating_point_range[0].step = step_;
}
try {
this->node_->declare_parameter(this->name_, rclcpp::ParameterValue(this->default_val_), descriptor);
}
catch (const rclcpp::exceptions::ParameterAlreadyDeclaredException& e) {
RCLCPP_WARN(this->node_->get_logger(), "Parameter [%s] already declared: %s",
this->name_.c_str(), e.what());
}
this->update(this->getParameter());
RCLCPP_INFO_STREAM(this->node_->get_logger(), this->namespace_ << "/" << this->name_ << ": " << this->toString(this->value()));
this->initialized_ = true;
}
U min_ = -10000;
U max_ = 10000;
U step_ = 0;
bool has_range_ = false;
friend class ParamHandler;
};
// Specialization for int (extends enum handling)
template <typename T>
class NumericIntParameter : public NumericParameter<T> {
using U = typename value_type_of<T>::type;
public:
NumericIntParameter(T* store, const std::string& ns, const std::string& name, T default_val,
const std::string& description, std::shared_ptr<rclcpp::Node> node)
: NumericParameter<T>(store, ns, name, default_val, description, node) {}
NumericIntParameter() = default;
NumericIntParameter(const NumericIntParameter& parameter) = default;
virtual ~NumericIntParameter() = default;
virtual NumericIntParameter<T>& callback(std::function<void(T)> callback) override {
NumericParameter<T>::callback(callback);
return *this;
}
virtual NumericIntParameter<T>& dynamic() override {
NumericParameter<T>::dynamic();
return *this;
}
virtual NumericIntParameter<T>& min(U min) override {
if (enums_.empty()) {
NumericParameter<T>::min(min);
}
return *this;
}
virtual NumericIntParameter<T>& max(U max) override {
if (enums_.empty()) {
NumericParameter<T>::max(max);
}
return *this;
}
virtual NumericIntParameter<T>& step(U step) override {
if (enums_.empty()) {
NumericParameter<T>::step(step);
}
return *this;
}
virtual NumericIntParameter<T>& enumerate(const std::vector<EnumOption<U>>& enums) {
enums_ = enums;
if (!enums_.empty()) {
this->has_range_ = false;
this->step_ = 0;
this->min_ = enums_.front().value;
this->max_ = enums_.front().value;
for (const auto& option: enums_) {
this->min_ = std::min(this->min_, option.value);
this->max_ = std::max(this->max_, option.value);
}
}
return *this;
}
const std::vector<EnumOption<U>>& enums() const {
return enums_;
}
bool checkEnum(const U& value) const {
for (const auto& option: enums_) {
if (option.value == value) {
return true;
}
}
return false;
}
// overload for vector
bool checkEnum(const std::vector<U>& values) const {
for (const auto& value : values) {
if (!checkEnum(value)) {
return false;
}
}
return true;
}
protected:
virtual bool update(const T& value, bool from_callback = false) {
if (!enums_.empty() && !checkEnum(value)) {
return false;
}
return NumericParameter<T>::update(value, from_callback);
}
// re-use the parent class toString method for any signatures
// not overridden in this class
// (.i.e. this allows us to re-use the parent class method for vector types)
using NumericParameter<T>::toString;
virtual std::string toString(const U& value) const override {
std::stringstream ss;
ss << value;
for (const auto& option: enums_) {
if (option.value == value) {
ss << " (" + option.name + ")";
}
}
return ss.str();
}
virtual void registerParam() override {
rcl_interfaces::msg::ParameterDescriptor descriptor;
descriptor.description = this->description_;
// generate enum description
if (!enums_.empty()) {
descriptor.description += "\nEnumeration:";
for (const auto& option: enums_) {
descriptor.description += "\n " + option.name + "(" + std::to_string(option.value) + "): " + option.description;
}
}
descriptor.read_only = !this->is_dynamic_;
if (this->has_range_ || !enums_.empty()) {
descriptor.integer_range.resize(1);
descriptor.integer_range[0].from_value = this->min_;
descriptor.integer_range[0].to_value = this->max_;
descriptor.integer_range[0].step = this->step_;
}
try {
this->node_->declare_parameter(this->name_, rclcpp::ParameterValue(this->default_val_), descriptor);
}
catch (const rclcpp::exceptions::ParameterAlreadyDeclaredException& e) {
RCLCPP_INFO(this->node_->get_logger(), "Parameter [%s] already declared: %s", this->name_.c_str(),
e.what());
}
this->update(this->getParameter());
RCLCPP_INFO_STREAM(this->node_->get_logger(), this->namespace_ << "/" << this->name_ << ": " << this->toString(this->value()));
this->initialized_ = true;
}
std::vector<EnumOption<U>> enums_;
friend class ParamHandler;
};
template class Parameter<bool>;
template class Parameter<std::vector<bool>>;
template class Parameter<std::string>;
template class Parameter<std::vector<std::string>>;
template class NumericIntParameter<int>;
template class NumericIntParameter<int64_t>;
template class NumericIntParameter<std::vector<int64_t>>;
template class NumericParameter<double>;
template class NumericParameter<std::vector<double>>;
typedef Parameter<bool> BoolParameter;
typedef Parameter<std::vector<bool>> BoolArrayParameter;
typedef Parameter<std::string> StringParameter;
typedef Parameter<std::vector<std::string>> StringArrayParameter;
typedef NumericIntParameter<int> SystemIntParameter; // support for legacy int parameters, they are stil int64_t in ROS, though
typedef NumericIntParameter<int64_t> IntParameter;
typedef NumericIntParameter<std::vector<int64_t>> IntArrayParameter;
typedef NumericParameter<double> DoubleParameter;
typedef NumericParameter<std::vector<double>> DoubleArrayParameter;
} // namespace hatchbed_common