-
Notifications
You must be signed in to change notification settings - Fork 79
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Autostarting lifecycle nodes and example launch file demo #430
base: rolling
Are you sure you want to change the base?
Conversation
Signed-off-by: Steve Macenski <[email protected]>
Signed-off-by: Steve Macenski <[email protected]>
Signed-off-by: Steve Macenski <[email protected]>
Signed-off-by: Steve Macenski <[email protected]>
Signed-off-by: Steve Macenski <[email protected]>
Signed-off-by: Steve Macenski <[email protected]>
Signed-off-by: Steve Macenski <[email protected]>
Signed-off-by: Steve Macenski <[email protected]>
@mjcarroll can I get a review on this now that we're back from the holidays? :-) |
That would be amazing to have this part of the standard launch! |
@mjcarroll can I request a review? ❤️ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few questions, but looking pretty good. Thanks for the contribution!
@@ -125,6 +125,7 @@ def __init__( | |||
exec_name: Optional[SomeSubstitutionsType] = None, | |||
parameters: Optional[SomeParameters] = None, | |||
remappings: Optional[SomeRemapRules] = None, | |||
autostart: Optional[bool] = False, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this need to be part of the Node action API? Doesn't it only need to be on a LifecycleNode action?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is mostly so that it doesn't crash if someone uses it. is_lifecycle_node
is set to false for this node type. The launch_ros.Node doesn't support the autostart usage. Its ignored.
I could remove it if you preferred. I just didn't want people to get stuck with these kinds of traces when the other 2 node types support it:
Traceback (most recent call last):
File "/root/jazzy_ws/./src/launch_ros/launch_ros/examples/lifecycle_autostart_pub_sub_launch.py", line 55, in <module>
main()
File "/root/jazzy_ws/./src/launch_ros/launch_ros/examples/lifecycle_autostart_pub_sub_launch.py", line 33, in main
talker_node = launch_ros.actions.Node(
^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/jazzy_ws/./src/launch_ros/launch_ros/examples/../launch_ros/actions/node.py", line 221, in __init__
super().__init__(cmd=cmd, **kwargs)
File "/opt/ros/rolling/lib/python3.12/site-packages/launch/actions/execute_process.py", line 240, in __init__
super().__init__(process_description=executable, **kwargs)
File "/opt/ros/rolling/lib/python3.12/site-packages/launch/actions/execute_local.py", line 188, in __init__
super().__init__(**kwargs)
TypeError: Action.__init__() got an unexpected keyword argument 'autostart'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean, it makes sense to me that a standard Node doesn't expect an autostart keyword. I would prefer it not to arguments it never uses.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be ok to check for the argument and raise a better error if you think it would help too.
@@ -43,6 +45,7 @@ def __init__( | |||
name: Optional[SomeSubstitutionsType] = None, | |||
namespace: Optional[SomeSubstitutionsType] = None, | |||
parameters: Optional[SomeParameters] = None, | |||
autostart: Optional[bool] = False, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as for Node action.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Composable nodes can be also lifecycle nodes -- see Nav2 :-)
This is actually the main application for which I'm opening this PR. Its very difficult to work with lifecycle-components in launch_ros. There's no technical way (before this) to transition up a component that's a lifecycle node within launch. The other events that are used by the launch_ros.LifecycleNode reject updates from other node types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I find it weird still that you'd pass an argument that may or may not be ignored. Wouldn't using the type system be good here, and having a composable lifecycle node type class to disambiguate it?
If we have a use case for a generic type that can be either a lifecycle node or something else, then maybe this is fine, but I believe you always know if a node is going to be lifecycle or not, otherwise you wouldn't pass autostart. Also, if you think the node is lifecycle, and so you pass autostart, but it actually isn't, wouldn't you want to catch that? Seems like a bug waiting to be missed.
@@ -34,7 +33,7 @@ def __init__( | |||
self, | |||
*, | |||
entities: SomeEntitiesType, | |||
target_lifecycle_node: LifecycleNode = None, | |||
target_lifecycle_node: Optional[SomeSubstitutionsType] = None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this need to be a SomeSubstitutionsType
? Not saying it's wrong, just curious what use case you have in mind?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And further, I'm not sure this works because this would essentially be a type name as a string, but before this was a value not a type, and we passed that instance of the object in to the event as event.action
. So maybe I do think this isn't right.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I needed this to not be specifically a LifecycleNode
due to circular dependencies using this within LifecycleNode itself for the transitioning. So, to make sure that it is still a lifecycle-compliant node, I used the Optional type and checked that it was a proper instance of a lifecycle-compliant node using is_lifecycle_node
(since components, lifecycle, and node in launch_ros can all be used to launch a rclcpp::LifecycleNode)
Im not sure this works
I can't comment confidently on the reasoning you have for it not working, but I can say from testing with the examples I provided that it does indeed work. I provided the testing samples here for that reason (and to augment the existing test samples of similar nature)
but before this was a value not a type
Un-confidently: Isn't LifecycleNode
a type and Optional[SomeSubstitutionsType]
a type? Is there something else you'd suggest I try here instead? I just used Optional[SomeSubstitutionsType]
based on what I saw elsewhere in the codebase for similar purposes, but I won't claim that this is right -- only that it appears to work and looks consistent to my naive eye with the rest of the codebase. My objective was to remove the circular dependencies, I don't know what non-LifecycleNode I should put here instead if not this if you think its wrong.
- target_lifecycle_node: LifecycleNode = None,
+ target_lifecycle_node: Optional[SomeSubstitutionsType] = None,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't LifecycleNode a type and Optional[SomeSubstitutionsType] a type?
I mean, yes, but that's like saying replacing rclcpp::Node
with std::string
in c++ is ok because they're both types...
If it's just an issue with circular dependencies then for the Type annotations you can just string quote the type, e.g.:
target_lifecycle_node: Optional[SomeSubstitutionsType] = None, | |
target_lifecycle_node: Optional['LifecycleNode'] = None, |
Or I think you can do something like this at the top of the file:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ... import LifecycleNode
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
btw, didn't mean to come off snarky with the c++ comparison, but that is the sort of what's happening with the SomeSubstitutionsType
, since that type essentially gets resolved into a string.
one, or even all lifecycle nodes, and it requests the targeted nodes | ||
to change state, see its documentation for more details. | ||
:param name: The name of the lifecycle node. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might want to clarify here whether or not the name needs to be fully qualified or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is copy+pasted from the lifecycle node abstracted into a manager to share with composition node. I don't really know one way or another, I'm not the original author. I could try to find out, but I'd prefer if this got grandfathered in as 'this was already here'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well you may have copy-pasted it, but it's still important for you and the users to know what needs to be passed here. If you're only forwarding it to the other constructor, then maybe you can simply reference it from here rather than copy-pasting the explanation.
|
||
def _on_change_state_event(self, context: launch.LaunchContext) -> None: | ||
typed_event = cast(ChangeState, context.locals.event) | ||
if not typed_event.lifecycle_node_matcher(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, if the user emits their own ChangeState
pointing to their LifecycleNode
action, then this will not match it right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is copy+pasted from the lifecycle node abstracted into a manager to share with composition node. I'd prefer not to make functional changes in this PR if that's OK with you. This has been the state of things previously and just reorganized to be shared among multiple node types
But w.r.t. this comment: No, actually! The subscription keeps them aligned:
launch_ros/launch_ros/launch_ros/utilities/lifecycle_event_manager.py
Lines 127 to 131 in 84c44de
self.__rclpy_subscription = node.create_subscription( | |
lifecycle_msgs.msg.TransitionEvent, | |
'{}/transition_event'.format(self.__node_name), | |
functools.partial(self._on_transition_event, context), | |
10) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is copy+pasted from the lifecycle node abstracted into a manager to share with composition node. I'd prefer not to make functional changes in this PR if that's OK with you. This has been the state of things previously and just reorganized to be shared among multiple node types
I get where you're coming from, but in this case your copy-paste is into a different class with a different type where you also use self
, so you're essentially changing the behavior even though it's a copy-paste because of that.
I don't see how the subscriptions keep them aligned? The typed_event.lifecycle_node_matcher
is user defined and may not look at the subscription at all. Plus the matcher explicitly expects a LifecycleNode
here not the type of self in your copy-pasted code:
def lifecycle_node_matcher(self) -> Callable[['LifecycleNode'], bool]: |
Here's an example that won't likely work:
lifecycle_node_matcher=launch.events.matches_action(talker_node), |
That is expecting the actions objects to match...
Signed-off-by: Steve Macenski <[email protected]>
@wjwwood I addressed all of your comments and made a fix by your review requests |
I sent a few responses, resolved a few others. Sorry it took so long to reply, I'll try to be quicker on the next iteration. Thanks again! |
This resolves #418 which implements autostarting lifecycle nodes. This is complete and ready for a review
You'll notice a couple of key changes worth explaining:
There is a new
is_lifecycle_node
attribute of the node and lifecycle nodes which is needed to remove a circular dependency on the LifecycleNode within theLifecycleTransition
class which only is type checking. I replace this type check with checking if a non-None object (1) has the attribute at all and (2) has a lifecycle attribute with appropriate logging between the difference of a non-lifecycle node being attempted vs a non-node. This has been tested as well to function using the demos.You will also notice that I refactored lifecycle node to have a separate util
LifecycleEventManager
, who handles the event emitting, handling, and topic listening for lifecycle nodes. This way, this can be used in lifecycle-components as well! No changes were made here except removing theself.__current_state
variable which was completely unused. TheLifecycleEventManager
is only created if the node requests autostart at Launch time and otherwise has no overhead nor exposes the interfaces/event handler/emitter if its a non-lifecycle node.The component nodes don't add a
/
before the node names with the namespaces. This messes with the node event matcher. I add in a leading/
so that theaction.node_name == node_name
which has the/
forcably applied in the node event matcherI tested
LifecycleNode
,ComposableNodeContainer
,LoadCompoableNodes
for all cases:autostart=True
autostart=False
, andautostart
field not supplied. You can also run any experiments you like using the 2 extra launch files I provide in the examples directory which shows all the features at work/