Skip to content

Commit a7ffe55

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 7a1ea4b commit a7ffe55

File tree

3 files changed

+200
-1
lines changed

3 files changed

+200
-1
lines changed

ros2doctor/ros2doctor/api/node.py

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