Skip to content

Conversation

@SprGrf
Copy link

@SprGrf SprGrf commented Jan 14, 2026

Description

This PR introduces a few basic wrapper functions to add, trigger and publish assignments for a consumer. There are some comments in places where I would definitely appreciate an opinion.

Review guidelines

Estimated Time of Review: 15 minutes

Checklist before merging:

  • Confirm that the relevant changelog(s) are up-to-date in case of any user-facing changes

@github-actions
Copy link

github-actions bot commented Jan 14, 2026

CLA Assistant Lite bot All contributors have signed the CLA ✍️ ✅

@SprGrf
Copy link
Author

SprGrf commented Jan 14, 2026

I have read and understood the Contributor License Agreement and hereby sign it

github-actions bot added a commit that referenced this pull request Jan 14, 2026
Copy link
Member

@domire8 domire8 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this, I would find it programmatically much easier to treat the assignment like a parameter:

// header
state_representation::ParameterMap assignment_map_;

// source
void add_assignment(name, ParameterType) {
  // checks that you already added correctly
  assignment_map_.set_parameter(std::shared_ptr<ParameterInterface>(name, type));
}

void trigger_assignment(name, value) {
  assignment_map_.set_parameter_value(name, value); // this will already handle all type compatibility for us and raise otherwise
  modulo_interfaces::msg::Assignment message;
  message.node = this->node_base_->get_fully_qualified_name();
  message.assignment = name;
  message.value = modulo_core::translators::write_parameter(assignment_map_.get_parameter(name)).to_parameter_msg().value;
}

No additional Assignment class would be required
@eeberhard

@SprGrf
Copy link
Author

SprGrf commented Jan 16, 2026

No additional Assignment class would be required

It's a fair point, since all it does for now is to check for a valid type. I guess it would be needed if we (plan to) add more functionality, such as checking for the last assigned value and avoid republishing (which has its own problems). Personally, I think it's more future-proof and extensible to keep the extra class.

@domire8
Copy link
Member

domire8 commented Jan 16, 2026

checking for the last assigned value and avoid republishing (which has its own problems)

I don't think we should have a mechanism to avoid republishing if the value hasn't changed. Thinking of an assignment like an output parameter, it should definitely publish any value if the user wants that. If we think of a classification component that has an assignment representing the detected class, I want it to publish the result every time (potentially at quite high intervals) it classifies an image

Copy link
Member

@eeberhard eeberhard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @domire8 that it would certainly be easier to store assignments like a parameter map. That way we get the type and the value, plus more code re-use.

Right now you don't store the last assignment value, which is technically fine because that was not a requirement for the API spec. But if you do store the assignment value, then it would allow us to have get_assignment for components to check for themselves what the last assignment was, and use that for example to conditionally decide not to republish the assignment.

It also makes using the Parameter message (instead of ParameterValue) for the message even simpler - we care about a named and typed value, which the parameter provides.

The analogy of course is set_predicate and get_predicate, where components could check if the predicate has changed or has a current true or false value without needing to store values explicitly or interact with the ROS API.

Comment on lines 195 to 198
if (assignment_name.empty()) {
RCLCPP_ERROR(this->node_logging_->get_logger(), "Failed to add assignment: Provide a non empty string as a name.");
return;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should validate or sanitize the assignment name. The schema allows assignments to have the following pattern: ^[a-zA-Z0-9]([a-zA-Z0-9_.-]?[a-zA-Z0-9])*$

I didn't check the existing code but I suppose this kind of sanitization already exists for signal or parameter names

const std::string& assignment_name, const state_representation::ParameterType& type) {
// Reusing parse_topic_name. Not very elegant. Could add a validate_assignment_name
// similar to services but it's a lot of code duplication
std::string parsed_name = modulo_utils::parsing::parse_topic_name(assignment_name);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fair only if the parsing in there allows the patterns specified in Enricos previous review

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as it's a valid subset of the permissible assignment names in the schema, it's fine to keep (and can be made more complete and comprehensive later). But if it allows invalid names, then it will be annoying to change later.

return true;
}

void ComponentInterface::add_assignment(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another design point, TBD with @eeberhard
Now that I see this, I think that the add_assignment should even be templated, like parameters.

add_assignment<double>("double_assignment")

This is more in line with parameters and templating we use across the components already. We would then add to the assignments_map_ with Parameter<T>(name) instead of ParameterInterface(name, type)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I am in favor.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I think that would be very nice for C++, and saves the user from having to deal with ParameterType as an arg.

Copy link
Member

@domire8 domire8 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good

/**
* @brief Get an assignment value.
* @tparam T The type of the assignment value
* @param assignment_name The name of the assignment to get
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could raise for two reasons (existence and type), we should catch it and throw an AssignmentException

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I expected this comment and I had a question. The get_parameter will throw a InvalidParameterException if it does not exist, and then the get_parameter_value a InvalidParameterCastException in case of casting to wrong type, or EmptyStateException if the parameter has not been set yet. So I was wondering whether I should catch and rethrow or leave as is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably best to catch all and throw and AssignmentException to avoid confusing a user with Parameter exceptions

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we could define InvalidAssignmentException and InvalidAssignmentCastException, this will probably have much less usage that get_parameter, so I would be in favor of a single InvalidAssignmentException covering both not existing and not matching type (i.e., catch InvalidParameterException and InvalidParameterCastException and rethrow as AssignmentException).

The case of EmptyStateException should be documented as a possible @throws but not caught IMO since it's already general and informative.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on the dedicated AssignmentException in general.
Just a thought on the empty state; I think a need might come up to poll the assignment and check if it has been used. Are we going to do this by catching the EmptyStateException, or we should make something dedicated?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a need might come up to poll the assignment and check if it has been used

since the component is the only one who gets to set assignments, there are plenty of other ways to check this (using a bool attribute as a flag for example). This is different to parameters whose values can be changed externally, or predicates whose values might be based on some callback function and change arbitrarily during runtime. So throwing an exception if the assignment has not been set is correct behavior in my opinion, while still keeping a distinction between the AssignmentError and EmptyStateException to disambiguate an undeclared assignment from an unused assignment.

}
try {
assignment->set_parameter_value<T>(assignment_value);
} catch (const state_representation::exceptions::InvalidParameterCastException&){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure this exception might be thrown? From what I can see, this is only used in get_parameter?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I tested, yes, that's what is thrown. Tests also succeed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting

SprGrf and others added 2 commits January 20, 2026 09:44
Co-authored-by: Dominic Reber <71256590+domire8@users.noreply.github.com>
Copy link
Member

@eeberhard eeberhard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good!

/**
* @brief Get an assignment value.
* @tparam T The type of the assignment value
* @param assignment_name The name of the assignment to get
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we could define InvalidAssignmentException and InvalidAssignmentCastException, this will probably have much less usage that get_parameter, so I would be in favor of a single InvalidAssignmentException covering both not existing and not matching type (i.e., catch InvalidParameterException and InvalidParameterCastException and rethrow as AssignmentException).

The case of EmptyStateException should be documented as a possible @throws but not caught IMO since it's already general and informative.

return true;
}

void ComponentInterface::add_assignment(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I think that would be very nice for C++, and saves the user from having to deal with ParameterType as an arg.

const std::string& assignment_name, const state_representation::ParameterType& type) {
// Reusing parse_topic_name. Not very elegant. Could add a validate_assignment_name
// similar to services but it's a lot of code duplication
std::string parsed_name = modulo_utils::parsing::parse_topic_name(assignment_name);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as it's a valid subset of the permissible assignment names in the schema, it's fine to keep (and can be made more complete and comprehensive later). But if it allows invalid names, then it will be annoying to change later.

SprGrf and others added 3 commits January 21, 2026 09:44
Co-authored-by: Dominic Reber <71256590+domire8@users.noreply.github.com>
Copy link
Member

@domire8 domire8 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me, pending approval of Enrico 👍

@domire8 domire8 changed the title feat(components): add assignment methods feat(components): add assignment methods (c++) Jan 21, 2026
Copy link
Member

@bpapaspyros bpapaspyros left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pending Enrico and a CHANGELOG entry perhaps

Copy link
Member

@eeberhard eeberhard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great! Sorry for the late final review.

pending Enrico and a CHANGELOG entry perhaps

It's going into a staging branch so as long as we can include the (combined) changelog entry before going into main it will be fine

@SprGrf SprGrf merged commit 5af7f22 into staging/assignments Jan 27, 2026
3 of 4 checks passed
@github-actions github-actions bot locked and limited conversation to collaborators Jan 27, 2026
@domire8 domire8 deleted the feat/assignment-methods branch January 27, 2026 09:32
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants