Skip to content

Commit 5622c13

Browse files
committed
ros2doctor: add node and parameter info to report
Add NodeCheck/NodeReport and ParameterCheck/ParameterReport so the ros2doctor report shows active nodes and their parameters. - Add `ros2doctor/api/node.py` (node discovery, NodeCheck, NodeReport) - Add `ros2doctor/api/parameter.py` (parameter discovery, ParameterCheck, ParameterReport) - Update entry points in `ros2doctor/setup.py`: - `ros2doctor.checks: NodeCheck` and `ParameterCheck` - `ros2doctor.report: NodeReport` and `ParameterReport` Why: This enhancement provides valuable insights into the active nodes and their parameters within a ROS 2 system. By including this information in the `ros2 doctor` report sections in the `ros2 doctor --report` output, giving users better visibility into the runtime ROS 2 system configuration. Fixes: #1090 Signed-off-by: BhuvanB404 <bhuvanb404@gmail.com> Signed-off-by: BhuvanB404 <bhuvanb1408@gmail.com>
1 parent 50547de commit 5622c13

3 files changed

Lines changed: 198 additions & 1 deletion

File tree

ros2doctor/ros2doctor/api/node.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Copyright 2025 Open Source Robotics Foundation, Inc.
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+
16+
from collections import defaultdict
17+
18+
from ros2cli.node.strategy import NodeStrategy
19+
from ros2doctor.api import DoctorCheck
20+
from ros2doctor.api import DoctorReport
21+
from ros2doctor.api import Report
22+
from ros2doctor.api import Result
23+
from ros2doctor.api.format import doctor_warn
24+
from ros2node.api import get_node_names
25+
26+
27+
def has_duplicates(values):
28+
"""Find out if there are any exact duplicates in a list of strings."""
29+
return len(set(values)) < len(values)
30+
31+
32+
class NodeCheck(DoctorCheck):
33+
"""Check for duplicate node names."""
34+
35+
def category(self):
36+
return 'node'
37+
38+
def check(self):
39+
result = Result()
40+
with NodeStrategy(None) as node:
41+
node_list = get_node_names(node=node, include_hidden_nodes=True)
42+
node_names = [n.full_name for n in node_list]
43+
if has_duplicates(node_names):
44+
name_counts = defaultdict(int)
45+
for name in node_names:
46+
name_counts[name] += 1
47+
48+
duplicates = [name for name, count in name_counts.items() if count > 1]
49+
for duplicate in duplicates:
50+
doctor_warn(f'Duplicate node name: {duplicate}')
51+
result.add_warning()
52+
return result
53+
54+
55+
class NodeReport(DoctorReport):
56+
"""Report node related information."""
57+
58+
def category(self):
59+
return 'node'
60+
61+
def report(self):
62+
report = Report('NODE LIST')
63+
with NodeStrategy(None) as node:
64+
node_list = get_node_names(node=node, include_hidden_nodes=True)
65+
node_names = [n.full_name for n in node_list]
66+
if not node_names:
67+
report.add_to_report('node count', 0)
68+
report.add_to_report('node', 'none')
69+
else:
70+
report.add_to_report('node count', len(node_names))
71+
for node_name in sorted(node_names):
72+
report.add_to_report('node', node_name)
73+
return report
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# Copyright 2025 Open Source Robotics Foundation, Inc.
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+
16+
from rcl_interfaces.srv import ListParameters
17+
import rclpy
18+
from ros2cli.node.direct import DirectNode
19+
from ros2cli.node.strategy import NodeStrategy
20+
from ros2doctor.api import DoctorCheck
21+
from ros2doctor.api import DoctorReport
22+
from ros2doctor.api import Report
23+
from ros2doctor.api import Result
24+
from ros2doctor.api.format import doctor_warn
25+
26+
27+
def call_list_parameters(node, node_name, namespace='/'):
28+
"""Call the list_parameters service for a specific node."""
29+
try:
30+
# Create service name and client for the target node's list_parameters service
31+
service_name = f"{namespace.rstrip('/')}/{node_name}/list_parameters"
32+
if service_name.startswith('//'):
33+
service_name = service_name[1:]
34+
35+
client = node.create_client(ListParameters, service_name)
36+
37+
if not client.wait_for_service(timeout_sec=1.0):
38+
return None
39+
40+
request = ListParameters.Request()
41+
future = client.call_async(request)
42+
43+
# Spin until the service call completes or times out
44+
rclpy.spin_until_future_complete(node, future, timeout_sec=2.0)
45+
46+
if future.done():
47+
response = future.result()
48+
node.destroy_client(client)
49+
return response
50+
else:
51+
node.destroy_client(client)
52+
return None
53+
54+
except Exception:
55+
return None
56+
57+
58+
class ParameterCheck(DoctorCheck):
59+
"""Check for nodes without parameter services."""
60+
61+
def category(self):
62+
return 'parameter'
63+
64+
def check(self):
65+
result = Result()
66+
with NodeStrategy(None) as node:
67+
try:
68+
node_names_and_namespaces = node.get_node_names_and_namespaces()
69+
except Exception:
70+
node_names_and_namespaces = []
71+
with DirectNode(None) as param_node:
72+
for node_name, namespace in node_names_and_namespaces:
73+
response = call_list_parameters(param_node.node, node_name, namespace)
74+
if response is None:
75+
full_name = f"{namespace.rstrip('/')}/{node_name}"
76+
doctor_warn(f'Node {full_name} has no parameter services.')
77+
result.add_warning()
78+
return result
79+
80+
81+
class ParameterReport(DoctorReport):
82+
"""Report parameter related information."""
83+
84+
def category(self):
85+
return 'parameter'
86+
87+
def report(self):
88+
report = Report('PARAMETER LIST')
89+
with NodeStrategy(None) as node:
90+
try:
91+
node_names_and_namespaces = node.get_node_names_and_namespaces()
92+
except Exception:
93+
node_names_and_namespaces = []
94+
if not node_names_and_namespaces:
95+
report.add_to_report('total nodes checked', 0)
96+
report.add_to_report('total parameter count', 0)
97+
report.add_to_report('parameter', 'none')
98+
return report
99+
100+
total_param_count = 0
101+
nodes_checked = 0
102+
103+
with DirectNode(None) as param_node:
104+
for node_name, namespace in sorted(node_names_and_namespaces):
105+
nodes_checked += 1
106+
response = call_list_parameters(param_node.node, node_name, namespace)
107+
if response and hasattr(response, 'result') and response.result:
108+
result = response.result
109+
param_names = result.names if hasattr(result, 'names') else []
110+
if param_names:
111+
total_param_count += len(param_names)
112+
full_name = f"{namespace.rstrip('/')}/{node_name}"
113+
114+
report.add_to_report('node', full_name)
115+
for param_name in sorted(param_names):
116+
report.add_to_report('parameter', param_name)
117+
118+
report.add_to_report('total nodes checked', nodes_checked)
119+
report.add_to_report('total parameter count', total_param_count)
120+
return report

ros2doctor/setup.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
'TopicCheck = ros2doctor.api.topic:TopicCheck',
4848
'QoSCompatibilityCheck = ros2doctor.api.qos_compatibility:QoSCompatibilityCheck',
4949
'PackageCheck = ros2doctor.api.package:PackageCheck',
50+
'NodeCheck = ros2doctor.api.node:NodeCheck',
51+
'ParameterCheck = ros2doctor.api.parameter:ParameterCheck',
5052
],
5153
'ros2doctor.report': [
5254
'PlatformReport = ros2doctor.api.platform:PlatformReport',
@@ -58,7 +60,9 @@
5860
'ActionReport = ros2doctor.api.action:ActionReport',
5961
'QoSCompatibilityReport = ros2doctor.api.qos_compatibility:QoSCompatibilityReport',
6062
'PackageReport = ros2doctor.api.package:PackageReport',
61-
'EnvironmentReport = ros2doctor.api.environment:EnvironmentReport'
63+
'EnvironmentReport = ros2doctor.api.environment:EnvironmentReport',
64+
'NodeReport = ros2doctor.api.node:NodeReport',
65+
'ParameterReport = ros2doctor.api.parameter:ParameterReport'
6266
],
6367
'ros2cli.extension_point': [
6468
'ros2doctor.verb = ros2doctor.verb:VerbExtension',

0 commit comments

Comments
 (0)