feat(level1): ROS2 完全体 12 包 / 80 测试 / 23 文档 / 工程化 / Docker 分组
This commit is contained in:
@@ -0,0 +1,775 @@
|
||||
# 编程规范 (CODING_STYLE) — ROS2 + Python + C++
|
||||
|
||||
> **目标**: 让本仓库所有代码符合 **ROS2 REP-2000** + **PEP 8** + **工业级实践**,
|
||||
> 从第一行代码就**专业、严谨、可维护**,为后续具身智能 / VLA 落地铺平基础。
|
||||
>
|
||||
> **适用范围**: 本仓库所有 Python (rclpy) / C++ (rclcpp) 代码 + 测试 + launch + 文档。
|
||||
>
|
||||
> **权威参考**:
|
||||
> - [ROS2 REP-2000: ROS 2 Design](https://www.ros.org/reps/rep-2002.html)
|
||||
> - [ROS2 Humble Code Style](https://docs.ros.org/en/humble/Contributing/Code-Style-Language-Versions.html)
|
||||
> - [PEP 8](https://peps.python.org/pep-0008/) / [PEP 257](https://peps.python.org/pep-0257/) / [PEP 484](https://peps.python.org/pep-0484/)
|
||||
> - [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html)
|
||||
> - [ROS2 Design: Parameter](https://design.ros2.org/articles/ros_parameters.html)
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [1. 设计原则](#1-设计原则)
|
||||
- [2. Python 规范 (rclpy)](#2-python-规范-rclpy)
|
||||
- [3. C++ 规范 (rclcpp)](#3-c-规范-rclcpp)
|
||||
- [4. 测试规范](#4-测试规范)
|
||||
- [5. ROS2 特定规范](#5-ros2-特定规范)
|
||||
- [6. 文档规范](#6-文档规范)
|
||||
- [7. Git 规范](#7-git-规范)
|
||||
- [8. 错误处理 + 日志规范](#8-错误处理--日志规范)
|
||||
- [9. 反模式 (Anti-Patterns)](#9-反模式-anti-patterns)
|
||||
|
||||
---
|
||||
|
||||
## 1. 设计原则
|
||||
|
||||
### 1.1 五大铁律
|
||||
|
||||
| # | 原则 | 含义 |
|
||||
|---|---|---|
|
||||
| 1 | **配置与代码解耦** | 参数 / YAML / launch 传值,代码不硬编码 |
|
||||
| 2 | **错误显式处理** | callback 异常用 try/except + `get_logger().error`,不静默吞 |
|
||||
| 3 | **可测试优先** | 每个包至少 1 个 pytest/gtest,关键路径 100% 覆盖 |
|
||||
| 4 | **接口契约清晰** | type hints / docstring / 错误码 三件套 |
|
||||
| 5 | **命名即文档** | `publisher_` 不是 `pub`,`timer_callback` 不是 `cb` |
|
||||
|
||||
### 1.2 SOLID 简化版
|
||||
|
||||
- **S** (Single Responsibility): 一个节点一个职责,不要混合"传感器读取 + 控制 + 日志上传"
|
||||
- **O** (Open-Closed): 通过参数和 launch 扩展,不改代码
|
||||
- **L** (Liskov): 子类可替换父类(虚函数 override)
|
||||
- **I** (Interface Segregation): 接口小而专,避免上帝节点
|
||||
- **D** (Dependency Inversion): 依赖抽象 (msg / service / action 类型),不依赖实现
|
||||
|
||||
---
|
||||
|
||||
## 2. Python 规范 (rclpy)
|
||||
|
||||
### 2.1 命名 (Naming)
|
||||
|
||||
| 类型 | 规则 | 例子 |
|
||||
|---|---|---|
|
||||
| 模块 | `snake_case` | `publisher_node.py` |
|
||||
| 类 | `PascalCase` | `ChatterPublisher` (不是 `MyPublisher`) |
|
||||
| 节点属性 | `snake_case_`(后缀下划线) | `self.publisher_`, `self.timer_` |
|
||||
| 私有方法 | `_snake_case` | `def _on_timer(self)` |
|
||||
| 常量 | `UPPER_SNAKE_CASE` | `DEFAULT_RATE_HZ = 1.0` |
|
||||
| ROS2 节点名 | `snake_case`,表示功能 | `chatter_publisher` (不是 `node1`) |
|
||||
| ROS2 话题名 | `snake_case`,可加前缀 | `/chatter`, `/robot1/joint_states` |
|
||||
| ROS2 参数名 | `snake_case` | `publish_rate_hz` (不是 `period_ms` 混用) |
|
||||
|
||||
**关键:节点属性后缀下划线**避免与 rclpy 内部方法同名(`timer`, `publisher`, `subscription` 都是 rclpy 内部属性)。
|
||||
|
||||
### 2.2 Type Hints (必填)
|
||||
|
||||
```python
|
||||
# Python 3.10+ 用内置类型,不用 typing.List/Dict
|
||||
from typing import List # 除非必要,否则不导入
|
||||
|
||||
class ChatterPublisher(Node):
|
||||
def __init__(self) -> None:
|
||||
super().__init__('chatter_publisher')
|
||||
self.declare_parameter('publish_rate_hz', 1.0)
|
||||
rate: float = self.get_parameter('publish_rate_hz').value
|
||||
self.publisher_: Publisher[String] = self.create_publisher(String, 'chatter', 10)
|
||||
```
|
||||
|
||||
### 2.3 Docstring (Google Style)
|
||||
|
||||
```python
|
||||
"""ChatterPublisher - 周期性发布 String 到 /chatter 话题。
|
||||
|
||||
设计思想:
|
||||
ROS2 Topic 是异步多对多单向通信,本节点演示:
|
||||
1. 参数声明 + 类型推断
|
||||
2. 周期性发布 + QoS
|
||||
3. 优雅退出(KeyboardInterrupt + rclpy.shutdown)
|
||||
|
||||
参考:
|
||||
- ROS2 设计稿 https://design.ros2.org/articles/topic_and_service.html
|
||||
- QoS 文档 https://docs.ros.org/en/humble/Concepts/About-Quality-of-Service.html
|
||||
"""
|
||||
```
|
||||
|
||||
类/方法的 docstring 模板:
|
||||
|
||||
```python
|
||||
class Foo:
|
||||
"""类的一句话描述。"""
|
||||
|
||||
def method(self, arg: int) -> bool:
|
||||
"""方法的一句话描述。
|
||||
|
||||
Args:
|
||||
arg: 参数描述。
|
||||
|
||||
Returns:
|
||||
返回值描述。
|
||||
|
||||
Raises:
|
||||
ValueError: 何时抛。
|
||||
"""
|
||||
```
|
||||
|
||||
### 2.4 节点模板 (必背)
|
||||
|
||||
```python
|
||||
"""<NodeName> — <一句话描述>"""
|
||||
from typing import List, Optional
|
||||
import rclpy
|
||||
from rclpy.node import Node
|
||||
from rclpy.publisher import Publisher
|
||||
from std_msgs.msg import String
|
||||
|
||||
|
||||
class MyNode(Node):
|
||||
"""节点描述。"""
|
||||
|
||||
DEFAULT_RATE_HZ: float = 1.0
|
||||
DEFAULT_TOPIC: str = 'chatter'
|
||||
QUEUE_SIZE: int = 10
|
||||
|
||||
def __init__(self, *, node_name: str = 'my_node') -> None:
|
||||
super().__init__(node_name)
|
||||
|
||||
# 1) 声明参数(类型由默认值推断)+ 描述符
|
||||
self.declare_parameter(
|
||||
'publish_rate_hz', self.DEFAULT_RATE_HZ,
|
||||
descriptor='发布频率 (Hz), 大于 0 的浮点数',
|
||||
)
|
||||
|
||||
# 2) 读取参数 + 构造组件
|
||||
rate: float = self.get_parameter('publish_rate_hz').value
|
||||
self.publisher_: Publisher[String] = self.create_publisher(
|
||||
String, self.DEFAULT_TOPIC, self.QUEUE_SIZE,
|
||||
)
|
||||
period: float = 1.0 / rate if rate > 0 else 1.0
|
||||
self.timer_ = self.create_timer(period, self._on_timer)
|
||||
|
||||
# 3) 内部状态(下划线)
|
||||
self._publish_count: int = 0
|
||||
|
||||
self.get_logger().info(f'MyNode started: rate={rate}Hz')
|
||||
|
||||
def _on_timer(self) -> None:
|
||||
"""定时器回调(下划线=内部方法)。"""
|
||||
msg = String()
|
||||
msg.data = f'Hello #{self._publish_count}'
|
||||
self.publisher_.publish(msg)
|
||||
self._publish_count += 1
|
||||
|
||||
|
||||
def main(args: Optional[List[str]] = None) -> None:
|
||||
"""ROS2 节点入口(标准模板)。"""
|
||||
rclpy.init(args=args)
|
||||
try:
|
||||
node = MyNode()
|
||||
rclpy.spin(node)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
if rclpy.ok():
|
||||
rclpy.shutdown()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
```
|
||||
|
||||
### 2.5 包结构 (ament_python)
|
||||
|
||||
```
|
||||
src/<package_name>/
|
||||
├── package.xml # ROS2 包元数据
|
||||
├── setup.py # Python 包配置 + entry_points
|
||||
├── setup.cfg # ament_python install 路径
|
||||
├── resource/<package_name> # 空文件,只用于 ament 索引
|
||||
├── <package_name>/ # Python 模块
|
||||
│ ├── __init__.py
|
||||
│ └── <node_module>.py
|
||||
├── launch/ # launch 文件 (被 colcon 安装)
|
||||
│ └── <name>_launch.py
|
||||
├── config/ # YAML 配置文件
|
||||
│ └── default.yaml
|
||||
├── urdf/ # (可选) URDF
|
||||
├── srv/ msg/ action/ # (可选) 自定义接口
|
||||
├── test/ # pytest 用例
|
||||
│ ├── conftest.py # 共享 fixture
|
||||
│ └── test_<unit>.py
|
||||
└── README.md # 包自描述文档(每个包必须有)
|
||||
```
|
||||
|
||||
### 2.6 setup.py 模板
|
||||
|
||||
```python
|
||||
from setuptools import setup
|
||||
import os
|
||||
from glob import glob
|
||||
|
||||
PACKAGE_NAME = '<package_name>'
|
||||
|
||||
setup(
|
||||
name=PACKAGE_NAME,
|
||||
version='0.1.0',
|
||||
packages=[PACKAGE_NAME],
|
||||
data_files=[
|
||||
('share/ament_index/resource_index/packages', ['resource/' + PACKAGE_NAME]),
|
||||
('share/' + PACKAGE_NAME, ['package.xml']),
|
||||
(os.path.join('share', PACKAGE_NAME, 'launch'), glob('launch/*.py')),
|
||||
(os.path.join('share', PACKAGE_NAME, 'config'), glob('config/*.yaml')),
|
||||
(os.path.join('share', PACKAGE_NAME, 'urdf'), glob('urdf/*')),
|
||||
],
|
||||
install_requires=['setuptools'],
|
||||
zip_safe=True,
|
||||
maintainer='<name>',
|
||||
maintainer_email='<email>',
|
||||
description='<一句话描述>',
|
||||
license='MIT',
|
||||
tests_require=['pytest'],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'<exec_name> = <package_name>.<module>:main',
|
||||
],
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### 2.7 package.xml 模板
|
||||
|
||||
```xml
|
||||
<?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><package_name></name>
|
||||
<version>0.1.0</version>
|
||||
<description><一句话描述,详细功能></description>
|
||||
<maintainer email="<email>"><name></maintainer>
|
||||
<license>MIT</license>
|
||||
|
||||
<!-- 运行依赖 -->
|
||||
<depend>rclpy</depend>
|
||||
<depend>std_msgs</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>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. C++ 规范 (rclcpp)
|
||||
|
||||
### 3.1 命名
|
||||
|
||||
| 类型 | 规则 | 例子 |
|
||||
|---|---|---|
|
||||
| 类 | `PascalCase` | `ChatterPublisher` |
|
||||
| 函数/方法 | `snake_case` (ROS2 风格) | `timer_callback()` |
|
||||
| 成员变量 | `snake_case_`(后缀下划线) | `publisher_`, `count_` |
|
||||
| 常量 | `kPascalCase` 或 `UPPER_SNAKE` | `kDefaultRate` 或 `DEFAULT_RATE` |
|
||||
| 命名空间 | `snake_case` | `my_robot::control` |
|
||||
|
||||
### 3.2 必须项
|
||||
|
||||
- **智能指针**: `std::shared_ptr<T>` + `std::make_shared<T>()`
|
||||
- **`override`**: 虚函数必须标
|
||||
- **`const`**: 不修改成员的方法加 `const`
|
||||
- **`explicit`**: 单参数构造加 `explicit`
|
||||
- **`#pragma once`**: 头文件用
|
||||
- **`nullptr`**: 不用 `NULL`
|
||||
|
||||
### 3.3 节点模板
|
||||
|
||||
```cpp
|
||||
// chatter_publisher.hpp
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "std_msgs/msg/string.hpp"
|
||||
|
||||
namespace my_robot
|
||||
{
|
||||
|
||||
class ChatterPublisher : public rclcpp::Node
|
||||
{
|
||||
public:
|
||||
explicit ChatterPublisher(const rclcpp::NodeOptions & options = rclcpp::NodeOptions());
|
||||
|
||||
private:
|
||||
void timer_callback();
|
||||
|
||||
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
|
||||
rclcpp::TimerBase::SharedPtr timer_;
|
||||
size_t count_;
|
||||
};
|
||||
|
||||
} // namespace my_robot
|
||||
```
|
||||
|
||||
```cpp
|
||||
// chatter_publisher.cpp
|
||||
#include "my_robot/chatter_publisher.hpp"
|
||||
|
||||
namespace my_robot
|
||||
{
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
ChatterPublisher::ChatterPublisher(const rclcpp::NodeOptions & options)
|
||||
: rclcpp::Node("chatter_publisher", options), count_(0)
|
||||
{
|
||||
this->declare_parameter<int>("period_ms", 500);
|
||||
this->declare_parameter<std::string>("topic", "chatter");
|
||||
|
||||
const int period_ms = this->get_parameter("period_ms").as_int();
|
||||
const std::string topic = this->get_parameter("topic").as_string();
|
||||
|
||||
publisher_ = this->create_publisher<std_msgs::msg::String>(topic, 10);
|
||||
timer_ = this->create_wall_timer(
|
||||
std::chrono::milliseconds(period_ms),
|
||||
std::bind(&ChatterPublisher::timer_callback, this));
|
||||
|
||||
RCLCPP_INFO(this->get_logger(),
|
||||
"ChatterPublisher started: topic=%s, period=%dms",
|
||||
topic.c_str(), period_ms);
|
||||
}
|
||||
|
||||
void ChatterPublisher::timer_callback()
|
||||
{
|
||||
auto msg = std_msgs::msg::String();
|
||||
msg.data = "Hello from C++, seq=" + std::to_string(count_++);
|
||||
publisher_->publish(msg);
|
||||
}
|
||||
|
||||
} // namespace my_robot
|
||||
|
||||
// main
|
||||
int main(int argc, char * argv[])
|
||||
{
|
||||
rclcpp::init(argc, argv);
|
||||
rclcpp::spin(std::make_shared<my_robot::ChatterPublisher>());
|
||||
rclcpp::shutdown();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 CMakeLists.txt 模板
|
||||
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(my_robot LANGUAGES CXX)
|
||||
|
||||
if(NOT CMAKE_CXX_STANDARD)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
endif()
|
||||
|
||||
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
add_compile_options(-Wall -Wextra -Wpedantic)
|
||||
endif()
|
||||
|
||||
set(THIS_PACKAGE_INCLUDE_DEPENDS
|
||||
rclcpp
|
||||
std_msgs
|
||||
)
|
||||
|
||||
# 头文件库
|
||||
add_library(${PROJECT_NAME}_core SHARED
|
||||
src/chatter_publisher.cpp
|
||||
)
|
||||
target_include_directories(${PROJECT_NAME}_core PUBLIC src)
|
||||
ament_target_dependencies(${PROJECT_NAME}_core ${THIS_PACKAGE_INCLUDE_DEPENDS})
|
||||
|
||||
# 可执行文件
|
||||
add_executable(chatter_publisher src/main.cpp)
|
||||
target_link_libraries(chatter_publisher ${PROJECT_NAME}_core)
|
||||
|
||||
# 安装
|
||||
install(TARGETS chatter_publisher
|
||||
DESTINATION lib/${PROJECT_NAME}
|
||||
)
|
||||
install(DIRECTORY launch config
|
||||
DESTINATION share/${PROJECT_NAME}
|
||||
)
|
||||
ament_package()
|
||||
```
|
||||
|
||||
### 3.5 测试 (gtest)
|
||||
|
||||
```cpp
|
||||
// test/test_chatter_publisher.cpp
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "my_robot/chatter_publisher.hpp"
|
||||
|
||||
class ChatterPublisherTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
static void SetUpTestSuite() { rclcpp::init(0, nullptr); }
|
||||
static void TearDownTestSuite() { rclcpp::shutdown(); }
|
||||
};
|
||||
|
||||
TEST_F(ChatterPublisherTest, ConstructsWithDefaults)
|
||||
{
|
||||
auto node = std::make_shared<my_robot::ChatterPublisher>();
|
||||
EXPECT_EQ(node->get_name(), std::string("chatter_publisher"));
|
||||
EXPECT_EQ(node->get_parameter("period_ms").as_int(), 500);
|
||||
EXPECT_EQ(node->get_parameter("topic").as_string(), std::string("chatter"));
|
||||
}
|
||||
|
||||
TEST_F(ChatterPublisherTest, PublishesMessages)
|
||||
{
|
||||
auto node = std::make_shared<my_robot::ChatterPublisher>();
|
||||
auto exec = std::make_shared<rclcpp::executors::SingleThreadedExecutor>();
|
||||
exec->add_node(node);
|
||||
|
||||
const auto end = std::chrono::steady_clock::now() + std::chrono::seconds(1);
|
||||
while (std::chrono::steady_clock::now() < end) {
|
||||
exec->spin_some(std::chrono::milliseconds(50));
|
||||
}
|
||||
SUCCEED();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 测试规范
|
||||
|
||||
### 4.1 测试金字塔
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ E2E (1-3) │ ← launch_testing + 真实场景
|
||||
├─────────────┤
|
||||
│ Integ (4-8) │ ← 同进程 spin + DDS
|
||||
├─────────────┤
|
||||
│ Unit (10+) │ ← 纯函数 / 参数声明 / 消息构造
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
### 4.2 pytest 模板 (conftest.py)
|
||||
|
||||
```python
|
||||
"""共享 fixture - 整个仓库所有 Python 包共用一套模式。"""
|
||||
from typing import Iterator
|
||||
import pytest
|
||||
import rclpy
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def ros_context() -> Iterator[None]:
|
||||
"""整个 session 共享 rclpy 上下文(避免反复 init/shutdown 引发 bug)。"""
|
||||
rclpy.init()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if rclpy.ok():
|
||||
rclpy.shutdown()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def node(ros_context: None) -> Iterator:
|
||||
"""每个测试一个独立节点实例。"""
|
||||
from <package>.<module> import MyNode
|
||||
instance = MyNode()
|
||||
try:
|
||||
yield instance
|
||||
finally:
|
||||
instance.destroy_node()
|
||||
```
|
||||
|
||||
### 4.3 测试命名
|
||||
|
||||
```python
|
||||
def test_<unit>_<scenario>_<expected>():
|
||||
"""例: test_publisher_init_with_default_rate_uses_1hz"""
|
||||
```
|
||||
|
||||
### 4.4 测试覆盖要求
|
||||
|
||||
| 层级 | 数量 | 内容 |
|
||||
|---|---|---|
|
||||
| Unit | ≥3 | 参数声明 / 默认值 / 关键方法调用 |
|
||||
| Integ | ≥2 | 同进程 spin / DDS roundtrip |
|
||||
| E2E | 1 (可选) | 完整 launch + 多节点 |
|
||||
|
||||
---
|
||||
|
||||
## 5. ROS2 特定规范
|
||||
|
||||
### 5.1 节点命名
|
||||
|
||||
- **节点名**: `snake_case`,表示功能(`chatter_publisher` 不是 `node1`)
|
||||
- **节点必须有 docstring**: 一句话说清做什么
|
||||
- **节点类名 = 节点名 CamelCase**: `ChatterPublisher` ↔ `chatter_publisher`
|
||||
- **不混用**: `MyNode` 这种名字只用于基类,不要直接用
|
||||
|
||||
### 5.2 参数
|
||||
|
||||
- **参数名 `snake_case`**: `publish_rate_hz`, `topic_name`, `frame_id`
|
||||
- **带单位后缀**: `_hz`, `_ms`, `_sec`, `_bytes` (避免歧义)
|
||||
- **声明时给 `descriptor`**: 便于 `ros2 param describe`
|
||||
- **运行时不变参数**: `readonly=True`
|
||||
- **运行时可变**: 注册 `add_on_set_parameters_callback` 校验
|
||||
|
||||
### 5.3 消息 / Service / Action
|
||||
|
||||
- **优先标准接口**: `std_msgs` / `sensor_msgs` / `geometry_msgs` / `example_interfaces`
|
||||
- **必须自定义时**: 在自己包内 `msg/`, `srv/`, `action/`
|
||||
- **字段命名**: `snake_case`,带单位 (`velocity_mps`)
|
||||
- **不要嵌指针 / 引用类型**: 用 ID (`string object_id`) 而非 `string&`
|
||||
|
||||
### 5.4 QoS
|
||||
|
||||
- **默认 RELIABLE + KEEP_LAST(10)**: 跨语言互通零障碍
|
||||
- **传感器流**: BEST_EFFORT + KEEP_LAST(1)
|
||||
- **控制指令**: RELIABLE + KEEP_LAST(1) + DEADLINE
|
||||
- **状态发布**: TRANSIENT_LOCAL + KEEP_LAST(1)
|
||||
|
||||
### 5.5 Launch 文件
|
||||
|
||||
- **函数签名**: `def generate_launch_description() -> LaunchDescription`
|
||||
- **可配置参数**: 用 `LaunchConfiguration` + `DeclareLaunchArgument`
|
||||
- **路径**: `PathJoinSubstitution` + `FindPackageShare`
|
||||
- **嵌套**: `IncludeLaunchDescription` + `PythonLaunchDescriptionSource`
|
||||
- **节点命名空间**: 必要时 `PushRosNamespace`
|
||||
|
||||
### 5.6 TF
|
||||
|
||||
- **frame_id `snake_case`**: `base_link`, `gripper`, `camera_optical_frame`
|
||||
- **REP-103 约定**: x 前, y 左, z 上 (右手系)
|
||||
- **REP-105 语义**: `map` → `odom` → `base_link`
|
||||
|
||||
---
|
||||
|
||||
## 6. 文档规范
|
||||
|
||||
### 6.1 每个文件 docstring (必填)
|
||||
|
||||
```python
|
||||
"""<文件名> - <一句话功能描述>。
|
||||
|
||||
功能:
|
||||
- 列出要点 1
|
||||
- 列出要点 2
|
||||
|
||||
关键概念:
|
||||
- ROS2 概念 1
|
||||
- ROS2 概念 2
|
||||
|
||||
运行方式:
|
||||
ros2 run <pkg> <exec>
|
||||
|
||||
参考:
|
||||
- 官方文档链接
|
||||
"""
|
||||
```
|
||||
|
||||
### 6.2 包内 README.md (必填)
|
||||
|
||||
每个包必须有 `README.md`,包含:
|
||||
|
||||
1. **功能**(一句话)
|
||||
2. **关键概念**(表格)
|
||||
3. **运行**(代码块,3-5 种)
|
||||
4. **测试**(代码块)
|
||||
5. **深度学习链接**(指向 doc/ 下的文档)
|
||||
|
||||
### 6.3 commit message
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<body>
|
||||
|
||||
<footer>
|
||||
|
||||
类型: feat / fix / docs / style / refactor / test / chore
|
||||
例子:
|
||||
feat(py_pubsub): 重写 publisher 为工业级风格 + type hints
|
||||
fix(cpp_robot_tf2): 加 const-correct 与 override
|
||||
docs(doc/15-params): 新增参数系统深度文档
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Git 规范
|
||||
|
||||
- **分支命名**: `feat/<name>` / `fix/<name>` / `docs/<name>`
|
||||
- **commit**: 中文/英文都行,但 subject ≤ 72 字符
|
||||
- **不修改全局 git config**: 用 `git -c user.name=x -c user.email=y commit` 临时设
|
||||
- **.gitignore**: 必须包含 `build/`, `install/`, `log/`, `.venv/`, `__pycache__/`
|
||||
|
||||
---
|
||||
|
||||
## 8. 错误处理 + 日志规范
|
||||
|
||||
### 8.1 日志级别
|
||||
|
||||
| 级别 | 何时用 |
|
||||
|---|---|
|
||||
| DEBUG | 周期事件(默认不打印,生产可用 `--log-level DEBUG` 看) |
|
||||
| INFO | 启动 / 关闭 / 配置变更 |
|
||||
| WARN | 异常但可恢复 |
|
||||
| ERROR | 操作失败但节点继续 |
|
||||
| FATAL | 节点即将退出 |
|
||||
|
||||
### 8.2 错误处理
|
||||
|
||||
```python
|
||||
# ✅ 正确: callback 里 try/except + 日志
|
||||
def _on_timer(self) -> None:
|
||||
try:
|
||||
msg = self._build_message()
|
||||
self.publisher_.publish(msg)
|
||||
except Exception as exc:
|
||||
self.get_logger().error(f'publish failed: {exc}', exc_info=True)
|
||||
|
||||
# ❌ 错: 让异常冒泡,节点崩
|
||||
def _on_timer(self) -> None:
|
||||
msg = self._build_message()
|
||||
self.publisher_.publish(msg)
|
||||
```
|
||||
|
||||
### 8.3 优雅退出
|
||||
|
||||
```python
|
||||
def main(args=None):
|
||||
rclpy.init(args=args)
|
||||
try:
|
||||
node = MyNode()
|
||||
rclpy.spin(node)
|
||||
except KeyboardInterrupt:
|
||||
node.get_logger().info('KeyboardInterrupt → 退出')
|
||||
finally:
|
||||
if rclpy.ok():
|
||||
rclpy.shutdown()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 反模式 (Anti-Patterns)
|
||||
|
||||
### 9.1 ❌ 不要
|
||||
|
||||
```python
|
||||
# ❌ 1) 节点名不规范
|
||||
super().__init__('node1')
|
||||
|
||||
# ❌ 2) 硬编码
|
||||
self.publisher_ = self.create_publisher(String, 'chatter', 10) # topic 不可配
|
||||
|
||||
# ❌ 3) 无 type hints
|
||||
def callback(self, msg):
|
||||
pass
|
||||
|
||||
# ❌ 4) 无 docstring
|
||||
class MyNode(Node):
|
||||
def __init__(self):
|
||||
...
|
||||
|
||||
# ❌ 5) print 而不是日志
|
||||
print('starting...')
|
||||
|
||||
# ❌ 6) callback 里阻塞 / 抛异常
|
||||
def _on_timer(self):
|
||||
result = blocking_io_call()
|
||||
self.publisher_.publish(result) # 如果 blocking_io_call 抛 → 节点崩
|
||||
|
||||
# ❌ 7) 重复 init/shutdown
|
||||
def test_a():
|
||||
rclpy.init()
|
||||
...
|
||||
rclpy.shutdown()
|
||||
|
||||
def test_b():
|
||||
rclpy.init() # 第二次 init → 异常
|
||||
|
||||
# ❌ 8) 命名混淆
|
||||
self.pub = self.create_publisher(...) # pub 是 keyword 别用
|
||||
self.timer = self.create_timer(...) # timer 是 rclpy 内部属性,会冲突
|
||||
|
||||
# ❌ 9) 包名叫 launch (与 ROS2 系统包冲突)
|
||||
# 包名必须不能与 ROS2 自带包同名
|
||||
```
|
||||
|
||||
### 9.2 ✅ 要
|
||||
|
||||
```python
|
||||
# ✅ 1) 节点名表示功能
|
||||
super().__init__('chatter_publisher')
|
||||
|
||||
# ✅ 2) 参数化
|
||||
self.declare_parameter('topic', 'chatter')
|
||||
topic = self.get_parameter('topic').value
|
||||
|
||||
# ✅ 3) 完整 type hints
|
||||
def _on_timer(self) -> None:
|
||||
msg: String = self._build_message()
|
||||
|
||||
# ✅ 4) Google style docstring
|
||||
class ChatterPublisher(Node):
|
||||
"""ChatterPublisher - 周期性发布 String 到 /chatter 话题。"""
|
||||
|
||||
# ✅ 5) 用 get_logger
|
||||
self.get_logger().info('starting...')
|
||||
|
||||
# ✅ 6) callback 里 try/except
|
||||
def _on_timer(self) -> None:
|
||||
try:
|
||||
...
|
||||
except Exception as exc:
|
||||
self.get_logger().error(f'failed: {exc}')
|
||||
|
||||
# ✅ 7) conftest.py 共享 fixture
|
||||
# 在 conftest.py 里做 session-scope rclpy.init
|
||||
|
||||
# ✅ 8) 节点属性后缀下划线
|
||||
self.publisher_: Publisher[String] = self.create_publisher(...)
|
||||
self.timer_: Timer = self.create_timer(...)
|
||||
|
||||
# ✅ 9) 包名避让 ROS2 系统包
|
||||
# 用 bringup, my_robot, my_arm 等
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 自检清单 (提交前)
|
||||
|
||||
- [ ] 每个文件顶部有 docstring(功能 / 关键概念 / 运行方式 / 参考)
|
||||
- [ ] 类 / 方法有 Google-style docstring
|
||||
- [ ] 所有函数有 type hints
|
||||
- [ ] 节点名 `snake_case`,类名 `PascalCase`,属性后缀 `_`
|
||||
- [ ] 参数带 `descriptor` 描述
|
||||
- [ ] callback 有 try/except
|
||||
- [ ] 测试用 conftest.py 共享 fixture
|
||||
- [ ] 包内有 README.md
|
||||
- [ ] `colcon build` 通过
|
||||
- [ ] `colcon test` 100% 通过
|
||||
- [ ] commit message 含 type(scope): subject
|
||||
|
||||
---
|
||||
|
||||
**违反任何一条,代码不得合并。**
|
||||
**一切为了:专业 / 严谨 / 可维护 / 为后续 VLA 落地铺路。**
|
||||
Reference in New Issue
Block a user