# 编程规范 (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 """ — <一句话描述>""" 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.xml # ROS2 包元数据 ├── setup.py # Python 包配置 + entry_points ├── setup.cfg # ament_python install 路径 ├── resource/ # 空文件,只用于 ament 索引 ├── / # Python 模块 │ ├── __init__.py │ └── .py ├── launch/ # launch 文件 (被 colcon 安装) │ └── _launch.py ├── config/ # YAML 配置文件 │ └── default.yaml ├── urdf/ # (可选) URDF ├── srv/ msg/ action/ # (可选) 自定义接口 ├── test/ # pytest 用例 │ ├── conftest.py # 共享 fixture │ └── test_.py └── README.md # 包自描述文档(每个包必须有) ``` ### 2.6 setup.py 模板 ```python from setuptools import setup import os from glob import glob 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='', maintainer_email='', description='<一句话描述>', license='MIT', tests_require=['pytest'], entry_points={ 'console_scripts': [ ' = .:main', ], }, ) ``` ### 2.7 package.xml 模板 ```xml 0.1.0 <一句话描述,详细功能> MIT rclpy std_msgs ament_copyright ament_flake8 ament_pep257 python3-pytest ament_python ``` --- ## 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` + `std::make_shared()` - **`override`**: 虚函数必须标 - **`const`**: 不修改成员的方法加 `const` - **`explicit`**: 单参数构造加 `explicit` - **`#pragma once`**: 头文件用 - **`nullptr`**: 不用 `NULL` ### 3.3 节点模板 ```cpp // chatter_publisher.hpp #pragma once #include #include #include #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::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("period_ms", 500); this->declare_parameter("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(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()); 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 #include #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(); 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(); auto exec = std::make_shared(); 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 . import MyNode instance = MyNode() try: yield instance finally: instance.destroy_node() ``` ### 4.3 测试命名 ```python def test___(): """例: 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 参考: - 官方文档链接 """ ``` ### 6.2 包内 README.md (必填) 每个包必须有 `README.md`,包含: 1. **功能**(一句话) 2. **关键概念**(表格) 3. **运行**(代码块,3-5 种) 4. **测试**(代码块) 5. **深度学习链接**(指向 doc/ 下的文档) ### 6.3 commit message ``` ():