init: ROS2 learning suite
This commit is contained in:
@@ -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',
|
||||
),
|
||||
])
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -0,0 +1,4 @@
|
||||
[develop]
|
||||
script_dir=$base/lib/py_srv
|
||||
[install]
|
||||
install_scripts=$base/lib/py_srv
|
||||
@@ -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',
|
||||
],
|
||||
},
|
||||
)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user