Skip to content

Commit 074c917

Browse files
authored
Merge pull request #56 from Kodo-Robotics/nav2_localization_bug
Navigation2 Localization Launch File Support
2 parents 7e49a95 + 2097220 commit 074c917

20 files changed

Lines changed: 794 additions & 29 deletions

CHANGELOG

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
77

88
---
99

10+
## [0.1.9] - 2025-08-12
11+
12+
### Added
13+
- Parser support for `EqualsSubstitution` and `NotEqualsSubstitution`
14+
- Test coverage for `nav2_localization.launch.py`
15+
- LaunchMap Watermark
16+
17+
### Fixed
18+
- Composable node containers visualization without being called in launch description
19+
1020
## [0.1.8] - 2025-08-08
1121

1222
### Added

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "launchmap",
33
"displayName": "LaunchMap",
44
"description": "Visualize ROS2 Launch Files",
5-
"version": "0.1.8",
5+
"version": "0.1.9",
66
"publisher": "kodorobotics",
77
"icon": "assets/launchmap-logo.png",
88
"bugs": {

parser/context.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,17 @@ def current_namespace(self) -> str | None:
8989

9090
## Composable node groups
9191

92+
def has_composable_node_group(self, container_name: str):
93+
return container_name in self.composable_node_groups
94+
9295
def register_composable_node_group(self, container_name: str, container_metadata: dict):
9396
self.composable_node_groups[container_name].update(container_metadata)
9497

9598
def extend_composable_node_group(self, container_name: str, nodes):
9699
self.composable_node_groups[container_name]["composable_nodes"].extend(nodes)
97100

98-
def get_composable_node_groups(self) -> list[dict]:
99-
results = []
101+
def get_composable_node_groups(self) -> dict:
102+
results = {}
100103
for name, data in self.composable_node_groups.items():
101104
if not data["composable_nodes"]:
102105
continue
@@ -109,8 +112,7 @@ def get_composable_node_groups(self) -> list[dict]:
109112
if value not in (None, "", [], {}):
110113
entry[key] = value
111114

112-
results.append(entry)
113-
115+
results[name] = entry
114116
return results
115117

116118
## Utility

parser/entrypoint/parser_runner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def parse_launch_file(filepath: str) -> dict:
6464
),
6565
"environment_variables": context.introspection.get_environment_variables(),
6666
"python_expressions": context.introspection.get_python_expressions(),
67-
"composable_node_containers": sorted(context.get_composable_node_groups()),
67+
"composable_containers": context.get_composable_node_groups(),
6868
"additional_components": context.introspection.get_registered_entities(),
6969
}
7070

parser/entrypoint/user_interface.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from parser.entrypoint.parser_runner import parse_launch_file
1616
from parser.parser.introspection_utils import (
17+
collect_composable_node_containers,
1718
collect_environment_variable_usages,
1819
collect_event_handler_usages,
1920
collect_launch_config_usages,
@@ -29,10 +30,8 @@ def parse_and_format_launch_file(filepath: str) -> dict:
2930
"""
3031
raw = parse_launch_file(filepath)
3132
grouped = group_entities_by_type(raw["parsed"] + raw["additional_components"])
32-
33-
composable_node_containers = raw.get("composable_node_containers")
34-
if composable_node_containers:
35-
grouped["composable_nodes_container"] = composable_node_containers
33+
34+
grouped = collect_composable_node_containers(grouped, raw["composable_containers"])
3635

3736
launch_argument_usages = collect_launch_config_usages(grouped)
3837
if launch_argument_usages:
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Copyright (c) 2025 Kodo Robotics
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import ast
16+
17+
from parser.context import ParseContext
18+
from parser.parser.postprocessing import simplify_launch_configurations
19+
from parser.parser.registry import register_handler
20+
from parser.resolution.utils import resolve_call_signature
21+
22+
23+
@register_handler("EqualsSubstitution", "launch.substitutions.EqualsSubstitution")
24+
def handle_equals_substitution(node: ast.Call, context: ParseContext) -> dict:
25+
args, _ = resolve_call_signature(node, context.engine)
26+
if len(args) != 2:
27+
raise ValueError("EqualsSubstitution must receive exactly two arguments.")
28+
29+
left, right = (simplify_launch_configurations(arg) for arg in args)
30+
return f"${{EqualsSubstitution:{left}, {right}}}"
31+
32+
33+
@register_handler("NotEqualsSubstitution", "launch.substitutions.NotEqualsSubstitution")
34+
def handle_not_equals_substitution(node: ast.Call, context: ParseContext) -> dict:
35+
args, _ = resolve_call_signature(node, context.engine)
36+
if len(args) != 2:
37+
raise ValueError("NotEqualsSubstitution must receive exactly two arguments.")
38+
39+
left, right = (simplify_launch_configurations(arg) for arg in args)
40+
return f"${{NotEqualsSubstitution:{left}, {right}}}"

parser/parser/handlers/group_handler.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,14 @@ def handle_group_action(node: ast.Call, context: ParseContext) -> dict:
3434
resolved_flat = flatten_once(raw_expr)
3535

3636
namespace = None
37+
parameters = []
3738
actions = []
3839
for item in resolved_flat:
39-
if isinstance(item, dict) and item.get("type") == "PushRosNamespace":
40+
if isinstance(item, dict) and item.get("type") == "PushROSNamespace":
4041
namespace = item.get("namespace")
4142
context.push_namespace(namespace)
43+
elif isinstance(item, dict) and item.get("type") == "SetParameter":
44+
parameters.append({item.get("name"): item.get("value")})
4245
else:
4346
actions.append(item)
4447

@@ -55,5 +58,7 @@ def handle_group_action(node: ast.Call, context: ParseContext) -> dict:
5558

5659
if namespace:
5760
result["namespace"] = namespace
61+
if parameters:
62+
result["parameters"] = parameters
5863

5964
return result

parser/parser/handlers/load_composable_node_handler.py renamed to parser/parser/handlers/load_composable_nodes_handler.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import ast
1616

1717
from parser.context import ParseContext
18+
from parser.parser.postprocessing import simplify_launch_configurations
1819
from parser.parser.registry import register_handler
1920
from parser.parser.utils.common import flatten_once, group_entities_by_type
2021
from parser.resolution.utils import resolve_call_signature
@@ -30,13 +31,25 @@ def handle_load_composable_nodes(node: ast.Call, context: ParseContext) -> dict:
3031
grouped = group_entities_by_type(resolved_flat)
3132
composable_nodes = grouped.get("unattached_composable_nodes", [])
3233

34+
# Add additional metadata to composable nodes
35+
condition = kwargs.get("condition", {})
36+
if condition:
37+
for idx, _ in enumerate(composable_nodes):
38+
composable_nodes[idx].update({"condition": condition})
39+
3340
# Determine target container
34-
target_container = kwargs.get("target_container")
41+
target_container = simplify_launch_configurations(kwargs.get("target_container"))
3542
if not target_container:
3643
raise ValueError("LoadComposableNodes requires a target_container to be specified.")
44+
if isinstance(target_container, list):
45+
target_container = "".join(target_container)
3746

3847
# Ensure group is registered
48+
first_instance = not context.has_composable_node_group(target_container)
3949
context.register_composable_node_group(target_container, {"target_container": target_container})
4050
context.extend_composable_node_group(target_container, composable_nodes)
4151

52+
if first_instance:
53+
return {"type": "ComposableNodeContainer", "target_container": target_container}
54+
4255
return {"type": "LoadComposableNodes", "target_container": target_container}

parser/parser/handlers/push_ros_namespace_handler.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@
1919
from parser.resolution.utils import resolve_call_signature
2020

2121

22-
@register_handler("PushRosNamespace", "launch_ros.actions.PushRosNamespace")
22+
@register_handler("PushROSNamespace", "launch_ros.actions.PushROSNamespace",
23+
"PushRosNamespace", "launch_ros.actions.PushRosNamespace")
2324
def handle_push_ros_namespace(node: ast.Call, context: ParseContext):
2425
args, kwargs = resolve_call_signature(node, context.engine)
2526

2627
ns = args[0] if args else kwargs.get("namespace")
27-
return {"type": "PushRosNamespace", "namespace": ns}
28+
return {"type": "PushROSNamespace", "namespace": ns}

0 commit comments

Comments
 (0)