init: ROS2 learning suite

This commit is contained in:
xs
2026-08-03 18:09:35 +08:00
commit 5ef38ab508
95 changed files with 13322 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
"""
srv_launch.py —— 启动 add_two_ints 服务端的 launch 文件。
注意:client 不在这里启动,因它是"一次性调用",通常在终端或脚本里跑。
client 用法:
source install/setup.bash
ros2 run py_srv add_two_ints_client 1 2 # 默认参数
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 3, b: 5}"
"""
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='py_srv',
executable='add_two_ints_server',
name='add_two_ints_server_py',
output='screen',
),
])
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0"?>
<?xml-model
href="http://download.ros.org/schema/package_format3.xsd"
schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>py_srv</name>
<version>0.1.0</version>
<description>Python service demo: AddTwoInts server + client</description>
<maintainer email="dev@example.com">xs</maintainer>
<license>Apache-2.0</license>
<depend>rclpy</depend>
<depend>example_interfaces</depend>
<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>python3-pytest</test_depend>
<export>
<build_type>ament_python</build_type>
</export>
</package>
View File
+64
View File
@@ -0,0 +1,64 @@
"""
add_two_ints_client.py —— ROS2 Python 服务端调用方(Service Client)。
Client API:
Client = node.create_client(srv_type, srv_name):
创建一个"客户端",绑定到指定 srv_name 的服务端。
client.wait_for_service(timeout_sec=...) ->
阻塞等服务端上线;ROS2 网络发现需要 1-2s。
request = srv_type.Request():
构造请求对象,填写字段。
future = client.call_async(request) -> Future:
异步调用,返回 Future;不阻塞主线程。
rclpy.spin_until_future_complete(node, future, timeout_sec=...):
在节点事件循环里等 future 完成(或超时),取回 response。
"""
import sys
import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts
class AddTwoIntsClient(Node):
def __init__(self):
super().__init__('add_two_ints_client_py')
self.client = self.create_client(AddTwoInts, 'add_two_ints')
# wait_for_service:阻塞到服务端上线或超时(秒)。
# 这里给 5 秒,通常 1-2s 内 ROS2 完成 DDS discovery。
while not self.client.wait_for_service(timeout_sec=1.0):
self.get_logger().info('waiting for add_two_ints service...')
def call(self, a, b):
req = AddTwoInts.Request()
req.a = a
req.b = b
# call_async 返回 Future,由 spin_until_future_complete 触发回调。
future = self.client.call_async(req)
# spin_until_future_complete:在节点事件循环里"短轮询"等到 future 完成。
rclpy.spin_until_future_complete(self, future, timeout_sec=5.0)
if future.result() is not None:
return future.result().sum
self.get_logger().error('service call failed')
return None
def main(args=None):
rclpy.init(args=args)
node = AddTwoIntsClient()
# 从命令行参数读取两个整数;默认 1 + 2。
a = int(sys.argv[1]) if len(sys.argv) > 1 else 1
b = int(sys.argv[2]) if len(sys.argv) > 2 else 2
result = node.call(a, b)
if result is not None:
node.get_logger().info(f'result: {a} + {b} = {result}')
print(f'RESULT: {a} + {b} = {result}')
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
+57
View File
@@ -0,0 +1,57 @@
"""
add_two_ints_server.py —— ROS2 Python 服务端(Service Server)。
ROS2 Service 是另一种通信模式,与 Topic 的区别:
Topic — 多对多、无连接、异步、单向(pub/sub)。
Service — 一对一、有连接、同步(可异步)、双向(request/response)。
适合"一次性调用 + 等结果"的场景,如拍照、开关机械臂、计算查询等。
关键 API:
Service[T_Request, T_Response]:
节点上的"服务句柄",由 create_service(...) 构造。
T_Request / T_Response:由 .srv 文件自动生成的 Python 类型;
本 demo 用 example_interfaces/srv/AddTwoInts,
含 a, b(int64) 与 sum(int64) 三个字段。
回调签名:
handler(req, response) -> response
handler 内部填 response 字段,ROS2 把它发回给 client。
"""
import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts
class AddTwoIntsServer(Node):
def __init__(self):
super().__init__('add_two_ints_server_py')
# create_service(srv_type, srv_name, callback):
# - srv_type:服务接口类(本例 AddTwoInts);
# - srv_name:客户端调用时使用的服务名;
# - callback:收到请求时由 ROS2 事件循环调用,
# 返回值就是发回 client 的 response。
self.srv = self.create_service(
AddTwoInts, 'add_two_ints', self.add_two_ints_callback)
self.get_logger().info('add_two_ints_server_py ready, waiting for requests...')
def add_two_ints_callback(self, request, response):
# request.a / request.b 是 AddTwoInts.Request 的 int64 字段。
response.sum = request.a + request.b
self.get_logger().info(f'incoming: a={request.a}, b={request.b} -> sum={response.sum}')
return response
def main(args=None):
rclpy.init(args=args)
node = AddTwoIntsServer()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
View File
+4
View File
@@ -0,0 +1,4 @@
[develop]
script_dir=$base/lib/py_srv
[install]
install_scripts=$base/lib/py_srv
+28
View File
@@ -0,0 +1,28 @@
from setuptools import find_packages, setup
package_name = 'py_srv'
setup(
name=package_name,
version='0.1.0',
packages=find_packages(exclude=['test']),
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
('share/' + package_name + '/launch', ['launch/srv_launch.py']),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='xs',
maintainer_email='dev@example.com',
description='Python service demo: AddTwoInts server + client',
license='Apache-2.0',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'add_two_ints_server = py_srv.add_two_ints_server:main',
'add_two_ints_client = py_srv.add_two_ints_client:main',
],
},
)
+43
View File
@@ -0,0 +1,43 @@
"""py_srv 单元测试:同进程 spin + 验证 Service 通信。"""
import time
import rclpy
import pytest
from py_srv.add_two_ints_server import AddTwoIntsServer
from py_srv.add_two_ints_client import AddTwoIntsClient
from example_interfaces.srv import AddTwoInts
@pytest.fixture(scope='module')
def ros_context():
rclpy.init()
yield
rclpy.shutdown()
def test_service_inproc_roundtrip(ros_context):
"""同进程内 server + client 互调,验证 a + b == sum。"""
server = AddTwoIntsServer()
client = AddTwoIntsClient()
exec_ = rclpy.executors.SingleThreadedExecutor()
exec_.add_node(server)
exec_.add_node(client)
req = AddTwoInts.Request()
req.a = 7
req.b = 35
future = client.client.call_async(req)
end = time.time() + 3.0
while not future.done() and time.time() < end:
exec_.spin_once(timeout_sec=0.05)
assert future.done(), 'service call did not complete in 3s'
assert future.result() is not None
assert future.result().sum == 42
exec_.remove_node(server)
exec_.remove_node(client)