关于 robotics-software-principles
面向机器人软件开发者的设计原则参考,涵盖与硬件交互系统的架构、时序、安全、可靠性和可维护性。
主要功能
- 01模块单一职责
- 02依赖硬件无关抽象
- 03隔离不同运行频率
- 04默认采用故障安全行为
- 05支持幂等命令
- 06结构化遥测
适用场景
- 01评审机器人软件架构
- 02规划从仿真到硬件的迁移
- 03检查通信中断时的安全行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ arpitg1304/ robotics-agent-skills --skill robotics-software-principles安装后在 Claude Code 中输入/robotics-software-principles确认已加载。
详细介绍
机器人软件设计原则
机器人软件为何与众不同
机器人代码面临大多数软件从未遇到过的约束:
- 物理后果 — Bug 不只是让进程崩溃,还可能让机器人撞上墙壁
- 实时期限 — 错过 1ms 控制循环的期限可能导致振荡或损坏
- 传感器不确定性 — 所有输入都有噪声、延迟,偶尔还会出错
- 硬件多样性 — 同一算法必须能在来自 5 家供应商的 10 种不同夹爪上运行
- 仿真到现实的差距 — 代码必须在仿真和真实硬件上以相同方式运行
- 长时间运行 — 机器人会运行数小时或数天;内存泄漏和漂移都很重要
- 安全关键性 — 无论软件状态如何,有些故障绝不能发生
这些约束要求严格的设计。以下原则将这些因素纳入考量。
原则 1:单一职责 — 一个模块,一项工作
每个模块(节点、类、函数)都应该只有一个变更理由。
为什么这对机器人很重要:如果感知模块还负责控制,那么相机驱动更新可能会导致机械臂控制器故障。在安全关键系统中,这种耦合不可接受。
# ❌ BAD: God module — perception + planning + control + logging
class RobotController:
def __init__(self):
self.camera = RealSenseCamera()
self.detector = YOLODetector()
self.planner = RRTPlanner()
self.arm = UR5Driver()
self.logger = DataLogger()
def run(self):
image = self.camera.capture()
objects = self.detector.detect(image)
path = self.planner.plan(objects[0].pose)
self.arm.execute(path)
self.logger.log(image, objects, path)
# If ANY of these changes, you touch this class
# ✅ GOOD: Separated responsibilities with clear interfaces
class PerceptionModule:
"""ONLY responsibility: raw sensor data → detected objects"""
def __init__(self, camera: CameraInterface, detector: DetectorInterface):
self.camera = camera
self.detector = detector
def get_detections(self) -> List[Detection]:
image = self.camera.capture()
return self.detector.detect(image)
class PlanningModule:
"""ONLY responsibility: goal + world state → trajectory"""
def __init__(self, planner: PlannerInterface):
self.planner = planner
def plan_to(self, target: Pose, obstacles: List[Obstacle]) -> Trajectory:
return self.planner.plan(target, obstacles)
class ExecutionModule:
"""ONLY responsibility: trajectory → hardware commands"""
def __init__(self, arm: ArmInterface):
self.arm = arm
def execute(self, trajectory: Trajectory) -> ExecutionResult:
return self.arm.follow_trajectory(trajectory)
测试:你能否在不使用“以及”的情况下描述一个模块的作用?如果不能,就拆分它。
原则 2:依赖倒置 — 依赖抽象,而非硬件
高层模块(规划、行为)绝不应依赖底层模块(驱动、硬件)。两者都应依赖抽象。
为什么这对机器人很重要:这是仿真到现实的基础。如果规划器直接导入 UR5Driver,就无法在仿真中运行。如果它依赖 ArmInterface,就能自由替换实现。
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List, Optional
import numpy as np
# ─── ABSTRACTIONS (the contracts) ────────────────────────────
class ArmInterface(ABC):
"""Abstract arm — every arm implementation must honor this contract"""
@abstractmethod
def get_joint_positions(self) -> np.ndarray:
"""Returns current joint positions in radians"""
...
@abstractmethod
def get_ee_pose(self) -> Pose:
"""Returns current end-effector pose"""
...
@abstractmethod
def move_to_joints(self, positions: np.ndarray,
velocity: float = 0.5) -> bool:
"""Move to joint positions. Returns True on success."""
...
@abstractmethod
def stop(self) -> None:
"""Immediately stop all motion"""
...
@property
@abstractmethod
def joint_limits(self) -> List[tuple]:
"""Returns [(min, max)] for each joint"""
...
class CameraInterface(ABC):
"""Abstract camera — any RGB camera must honor this"""
@abstractmethod
def capture(self) -> np.ndarray:
"""Returns (H, W, 3) uint8 RGB image"""
...
@abstractmethod
def get_intrinsics(self) -> CameraIntrinsics:
"""Returns camera intrinsic parameters"""
...
@property
@abstractmethod
def resolution(self) -> tuple:
"""Returns (width, height)"""
...
class GripperInterface(ABC):
@abstractmethod
def open(self, width: float = 1.0) -> bool: ...
@abstractmethod
def close(self, force: float = 0.5) -> bool: ...
@abstractmethod
def get_width(self) -> float: ...
@abstractmethod
def is_grasping(self) -> bool: ...
# ─── CONCRETE IMPLEMENTATIONS ────────────────────────────────
class UR5Arm(ArmInterface):
"""Real UR5 via RTDE protocol"""
def __init__(self, ip: str):
self.rtde = RTDEControl(ip)
self.rtde_receive = RTDEReceive(ip)
def get_joint_positions(self) -> np.ndarray:
return np.array(self.rtde_receive.getActualQ())
def move_to_joints(self, positions, velocity=0.5):
self.rtde.moveJ(positions.tolist(), velocity)
return True
def stop(self):
self.rtde.stopScript()
@property
def joint_limits(self):
return [(-2*np.pi, 2*np.pi)] * 6
class MuJoCoArm(ArmInterface):
"""Simulated arm in MuJoCo — SAME interface"""
def __init__(self, model_path: str, joint_names: List[str]):
self.model = mujoco.MjModel.from_xml_path(model_path)
self.data = mujoco.MjData(self.model)
self.joint_ids = [mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, n)
for n in joint_names]
def get_joint_positions(self) -> np.ndarray:
return np.array([self.data.qpos[jid] for jid in self.joint_ids])
def move_to_joints(self, positions, velocity=0.5):
# Simulate motion with position control
self.data.ctrl[:len(positions)] = positions
for _ in range(100):
mujoco.mj_step(self.model, self.data)
return True
def stop(self):
self.data.ctrl[:] = 0
# ─── HIGH-LEVEL CODE DEPENDS ONLY ON ABSTRACTIONS ────────────
class PickPlaceTask:
"""This class works with ANY arm + gripper + camera.
It never knows or cares if it's sim or real."""
def __init__(self, arm: ArmInterface, gripper: GripperInterface,
camera: CameraInterface, detector: DetectorInterface):
self.arm = arm
self.gripper = gripper
self.camera = camera
self.detector = detector
def execute(self, target_class: str) -> bool:
image = self.camera.capture()
detections = self.detector.detect(image)
target = next((d for d in detections if d.label == target_class), None)
if target is None:
return False
self.arm.move_to_joints(self.ik(target.pose))
self.gripper.close()
self.arm.move_to_joints(self.place_joints)
self.gripper.open()
return True
机器人中的依赖规则:
Application / Tasks
↓ depends on
Interfaces (ABC)
↑ implements
Hardware Drivers / Simulators
箭头指向内部。高层策略绝不导入底层驱动。
原则 3:开闭原则 — 扩展而不修改
模块应该对扩展开放,对修改关闭。通过添加新代码来增加新功能,而不是修改现有代码。
为什么这对机器人很重要:你会不断添加新传感器、新机器人和新任务。如果添加新相机需要修改感知流水线,就会破坏现有部署。
# ❌ BAD: Adding a new sensor requires modifying existing code
class PerceptionPipeline:
def process(self, sensor_type: str, data):
if sensor_type == 'realsense':
return self._process_realsense(data)
elif sensor_type == 'zed':
return self._process_zed(data)
elif sensor_type == 'oakd': # New sensor = modify this class
return self._process_oakd(data)
# ✅ GOOD: Plugin architecture — add sensors without touching core
class SensorPlugin(ABC):
"""Base class for all sensor plugins"""
@abstractmethod
def name(self) -> str: ...
@abstractmethod
def process(self, raw_data) -> ProcessedData: ...
@abstractmethod
def get_intrinsics(self) -> dict: ...
class RealSensePlugin(SensorPlugin):
def name(self): return 'realsense'
def process(self, raw_data):
# RealSense-specific processing
return ProcessedData(...)
class ZEDPlugin(SensorPlugin):
def name(self): return 'zed'
def process(self, raw_data):
# ZED-specific processing
return ProcessedData(...)
# Core pipeline never changes when you add sensors
class PerceptionPipeline:
def __init__(self):
self._plugins: dict[str, SensorPlugin] = {}
def register_sensor(self, plugin: SensorPlugin):
"""Extend the pipeline without modifying it"""
self._plugins[plugin.name()] = plugin
def process(self, sensor_name: str, data):
if sensor_name not in self._plugins:
raise ValueError(f"Unknown sensor: {sensor_name}")
return self._plugins[sensor_name].process(data)
# Adding OAK-D = add a file, register at startup. Zero changes to core.
class OAKDPlugin(SensorPlugin):
def name(self): return 'oakd'
def process(self, raw_data):
return ProcessedData(...)
pipeline = PerceptionPipeline()
pipeline.register_sensor(RealSensePlugin())
pipeline.register_sensor(OAKDPlugin()) # No core code changed
原则 4:接口隔离 — 小而专注的接口
不要强迫模块依赖它们并不使用的接口。多个小接口胜过一个大接口。
为什么这对机器人很重要:简单的 1-DOF 夹爪不应实现 6-DOF 灵巧手接口。固定相机不应实现云台方法。
# ❌ BAD: Fat interface — every camera must implement ALL of these
class CameraInterface(ABC):
@abstractmethod
def capture_rgb(self) -> np.ndarray: ...
@abstractmethod
def capture_depth(self) -> np.ndarray: ...
@abstractmethod
def capture_pointcloud(self) -> np.ndarray: ...
@abstractmethod
def set_exposure(self, value: float): ...
@abstractmethod
def set_pan_tilt(self, pan: float, tilt: float): ...
@abstractmethod
def stream_video(self) -> Iterator[np.ndarray]: ...
# A simple USB webcam can't do half of these!
# ✅ GOOD: Segregated interfaces — implement only what you support
class RGBCamera(ABC):
"""Any camera that produces RGB images"""
@abstractmethod
def capture_rgb(self) -> np.ndarray: ...
@property
@abstractmethod
def resolution(self) -> tuple: ...
class DepthCamera(ABC):
"""Cameras that also produce depth"""
@abstractmethod
def capture_depth(self) -> np.ndarray: ...
@abstractmethod
def get_depth_intrinsics(self) -> DepthIntrinsics: ...
class ControllableCamera(ABC):
"""Cameras with adjustable settings"""
@abstractmethod
def set_exposure(self, value: float): ...
@abstractmethod
def set_white_balance(self, value: float): ...
class PTZCamera(ABC):
"""Pan-tilt-zoom cameras"""
@abstractmethod
def set_pan_tilt(self, pan: float, tilt: float): ...
@abstractmethod
def set_zoom(self, level: float): ...
# A RealSense implements RGB + Depth, but not PTZ
class RealSenseD435(RGBCamera, DepthCamera, ControllableCamera):
def capture_rgb(self): ...
def capture_depth(self): ...
def set_exposure(self, value): ...
# No PTZ methods — it's not a PTZ camera!
# A webcam implements only RGB
class USBWebcam(RGBCamera):
def capture_rgb(self): ...
# Nothing else required
# Perception code that only needs RGB doesn't pull in depth dependencies
class ObjectDetector:
def __init__(self, camera: RGBCamera): # Only needs RGB
self.camera = camera
def detect(self) -> List[Detection]:
image = self.camera.capture_rgb()
return self.model.predict(image)
原则 5:里氏替换 — 实现可替换
接口的任何实现都必须能够替换其他实现,且调用方无需知晓。如果你的代码适用于 ArmInterface,那么它必须适用于任何实现该接口的机械臂。
为什么这对机器人很重要:仿真到现实的迁移、硬件更换和多机器人支持都依赖于此。
# ❌ BAD: Violates substitution — caller must know the implementation
class FrankaArm(ArmInterface):
def move_to_joints(self, positions, velocity=0.5):
if len(positions) != 7:
raise ValueError("Franka has 7 joints!") # Franka-specific
# ...
class UR5Arm(ArmInterface):
def move_to_joints(self, positions, velocity=0.5):
if len(positions) != 6:
raise ValueError("UR5 has 6 joints!") # UR5-specific
# ...
# Caller must know which arm it's using to pass correct joint count!
# This breaks substitutability.
# ✅ GOOD: Self-describing implementations
class ArmInterface(ABC):
@property
@abstractmethod
def num_joints(self) -> int: ...
@property
@abstractmethod
def joint_limits(self) -> List[tuple]: ...
@abstractmethod
def move_to_joints(self, positions: np.ndarray, velocity: float = 0.5) -> bool:
"""Positions must have length == self.num_joints"""
...
class FrankaArm(ArmInterface):
@property
def num_joints(self): return 7
def move_to_joints(self, positions, velocity=0.5):
assert len(positions) == self.num_joints
# ...
# Caller code is generic — works with any arm
def move_to_home(arm: ArmInterface):
home = np.zeros(arm.num_joints) # Queries the arm, doesn't assume
arm.move_to_joints(home)
替换测试:检查调用方代码的每一行。将 UR5 替换为 Franka,再替换为 SimArm。代码还能正常运行吗?如果不能,你的抽象就存在泄漏。
原则 6:速率分离 — 尊重时序边界
不同子系统以不同速率运行。绝不要将它们耦合起来。
Component Typical Rate Criticality
─────────────────────────────────────────────────
Safety monitor 1000 Hz HARD real-time
Joint controller 500-1000 Hz HARD real-time
Trajectory exec 100-200 Hz Firm real-time
State estimation 50-200 Hz Firm real-time
Perception 10-30 Hz Soft real-time
Planning 1-10 Hz Best effort
Task management 0.1-1 Hz Best effort
Logging 1-30 Hz Best effort
UI/Dashboard 1-10 Hz Best effort
# ❌ BAD: Perception blocks the control loop
class Robot:
def control_loop(self): # Must run at 100Hz = 10ms budget
image = self.camera.capture() # 5ms
objects = self.detector.detect(image) # 200ms ← BLOCKS!
pose = self.estimate_pose(objects) # 2ms
cmd = self.controller.compute(pose) # 0.1ms
self.arm.send_command(cmd) # 0.5ms
# Total: 207ms. Control runs at 5Hz instead of 100Hz!
# ✅ GOOD: Decoupled rates with async boundaries
class Robot:
def __init__(self):
self.latest_detections = []
self.detection_lock = threading.Lock()
# Perception runs in its own thread at its own rate
self.perception_thread = threading.Thread(
target=self._perception_loop, daemon=True)
self.perception_thread.start()
def _perception_loop(self):
"""Runs at ~10Hz — as fast as the detector allows"""
while self.running:
image = self.camera.capture()
detections = self.detector.detect(image)
with self.detection_lock:
self.latest_detections = detections
def control_loop(self):
"""Runs at 100Hz — NEVER blocked by perception"""
rate = Rate(100) # 10ms period
while self.running:
with self.detection_lock:
detections = self.latest_detections # Latest available
pose = self.estimate_pose(detections)
cmd = self.controller.compute(pose)
self.arm.send_command(cmd)
rate.sleep()
规则:如果子系统 A 比子系统 B 慢,A 必须通过缓冲区(话题、共享变量、队列)与 B 通信,绝不能直接调用。
原则 7:故障安全默认值 — 未经证实前,默认安全
每个模块都应默认采用尽可能安全的行为。安全不是后来添加的功能,而是默认状态,其他行为都应从此基础上逐步放宽。
# ❌ BAD: Unsafe defaults
class ArmController:
def __init__(self):
self.max_velocity = 3.14 # Full speed by default!
self.collision_check = False # Off by default!
self.workspace_limits = None # No limits by default!
# ✅ GOOD: Safe defaults — must explicitly opt into danger
class ArmController:
def __init__(self):
self.max_velocity = 0.1 # Crawl speed by default
self.collision_check = True # Always on
self.workspace_limits = DEFAULT_SAFE_WORKSPACE # Conservative box
self.require_enable = True # Must be explicitly enabled
self._enabled = False
def enable(self, operator_confirmed: bool = False):
"""Explicit enable step — requires operator confirmation for real hardware"""
if not operator_confirmed and not self.is_simulation:
raise SafetyError(
"Real hardware requires operator confirmation to enable")
self._enabled = True
def move_to(self, target: np.ndarray, velocity: float = None):
if not self._enabled:
raise SafetyError("Controller not enabled")
velocity = velocity or self.max_velocity
# Clamp velocity to safe range
velocity = min(velocity, self.max_velocity)
# Check workspace limits BEFORE moving
if not self.workspace_limits.contains(target):
raise WorkspaceViolation(f"Target {target} outside safe workspace")
# Check for collisions BEFORE moving
if self.collision_check:
if self.collision_detector.would_collide(target):
raise CollisionRisk(f"Collision predicted for target {target}")
return self._execute_move(target, velocity)
规则:当模块没有收到输入、收到无效输入或通信中断时会发生什么?它应该安全停止,而不是盲目继续运行。
class SafetyDefaults:
"""Centralized safe defaults for the entire system"""
# Communication loss → stop
HEARTBEAT_TIMEOUT_MS = 500
ACTION_ON_TIMEOUT = 'stop' # Not 'continue_last_command'
# Unknown state → stop
ACTION_ON_UNKNOWN_STATE = 'stop' # Not 'assume_safe'
# Sensor failure → stop
ACTION_ON_SENSOR_LOSS = 'stop' # Not 'use_last_reading'
# Joint limit approach → slow down
JOINT_LIMIT_MARGIN_RAD = 0.05 # Stop 0.05 rad before limit
VELOCITY_NEAR_LIMITS = 0.05 # Crawl near limits
# Default workspace (conservative bounding box)
WORKSPACE_MIN = np.array([-0.5, -0.5, 0.0]) # meters
WORKSPACE_MAX = np.array([0.5, 0.5, 0.8]) # meters
原则 8:配置优先于代码——将所有会变化的内容外置
任何可能因部署、机器人或环境不同而有所差异的内容,都应该放在配置中,而不是代码里。
# ❌ BAD: Hardcoded values scattered across files
class GraspPlanner:
def plan(self, object_pose):
approach_height = 0.15 # Magic number
grasp_depth = 0.02 # Magic number
if object_pose.z < 0.05: # Magic number
return None
# ✅ GOOD: Configuration-driven
# config/grasp_planner.yaml
# grasp_planner:
# approach_height_m: 0.15
# grasp_depth_m: 0.02
# min_object_height_m: 0.05
# max_grasp_width_m: 0.08
# approach_velocity: 0.1
# grasp_force_n: 10.0
@dataclass
class GraspConfig:
approach_height_m: float = 0.15
grasp_depth_m: float = 0.02
min_object_height_m: float = 0.05
max_grasp_width_m: float = 0.08
approach_velocity: float = 0.1
grasp_force_n: float = 10.0
@classmethod
def from_yaml(cls, path: str) -> 'GraspConfig':
with open(path) as f:
data = yaml.safe_load(f)
return cls(**data.get('grasp_planner', {}))
def validate(self):
assert self.approach_height_m > 0, "Approach height must be positive"
assert 0 < self.grasp_force_n < 100, "Force out of safe range"
class GraspPlanner:
def __init__(self, config: GraspConfig):
config.validate()
self.config = config
def plan(self, object_pose):
if object_pose.z < self.config.min_object_height_m:
return None
# ...
配置中应包含的内容:机器人 IP 地址、关节限位、传感器参数、安全阈值、工作空间边界、特定任务常量、文件路径、功能标志。
代码中应保留的内容:算法、控制逻辑、数据结构、接口定义、错误处理。
原则 9:幂等操作——可安全重试
每条命令都应能安全地发送两次。在机器人领域,网络中断、消息重复和重试都是常有的事。
# ❌ BAD: Non-idempotent — sending twice moves the robot twice as far
def move_relative(self, delta: np.ndarray):
current = self.get_position()
self.move_to(current + delta)
# If this message is sent twice due to a retry,
# the robot moves 2x the intended distance!
# ✅ GOOD: Idempotent — sending twice has the same effect as once
def move_to_absolute(self, target: np.ndarray, command_id: str):
if command_id == self._last_executed_command:
return # Already executed this command, skip
self._last_executed_command = command_id
self.move_to(target)
# Sending this twice is harmless — same target, same result
# ✅ GOOD: Idempotent gripper commands
def set_gripper(self, width: float):
"""Set gripper to absolute width — not open/close toggle"""
self.gripper.move_to_width(width)
# Calling set_gripper(0.04) ten times still results in 0.04m width
原则 10:全面观测——无法调试你看不到的东西
每个模块都应输出结构化遥测数据。当机器人在凌晨 2 点出现异常行为时,你能依靠的只有日志。
import structlog
from dataclasses import dataclass, asdict
logger = structlog.get_logger()
@dataclass
class PerceptionEvent:
timestamp: float
num_detections: int
processing_time_ms: float
frame_id: str
detector_confidence: float
class PerceptionModule:
def process(self, image):
t_start = time.monotonic()
detections = self.detector.detect(image)
t_elapsed = (time.monotonic() - t_start) * 1000
# Structured logging — machine-parseable
event = PerceptionEvent(
timestamp=time.time(),
num_detections=len(detections),
processing_time_ms=t_elapsed,
frame_id=image.header.frame_id,
detector_confidence=max(
(d.confidence for d in detections), default=0.0),
)
logger.info("perception.processed", **asdict(event))
# Performance warnings
if t_elapsed > 100:
logger.warning("perception.slow",
processing_time_ms=t_elapsed,
threshold_ms=100)
# Anomaly detection
if len(detections) == 0 and self._expected_objects > 0:
logger.warning("perception.no_detections",
expected=self._expected_objects,
image_mean=float(image.data.mean()))
return detections
应记录的内容:状态转换、命令执行、安全事件、性能指标、传感器健康状况、错误情况、配置变更。
记录方式:使用结构化键值对(而非 printf 字符串),并包含时间戳、严重级别和模块标识符。
原则 11:可组合性——用简单行为构建复杂行为
将模块设计为可组合的构建块。复杂的机器人行为应通过组合简单且经过测试的原语实现。
# Primitive skills — simple, tested, reusable
class MoveTo(Skill):
"""Move end-effector to a target pose"""
def execute(self, target: Pose) -> bool: ...
class Grasp(Skill):
"""Close gripper with force control"""
def execute(self, force: float = 10.0) -> bool: ...
class Release(Skill):
"""Open gripper"""
def execute(self) -> bool: ...
class LookAt(Skill):
"""Point camera at a target"""
def execute(self, target: Point) -> bool: ...
class Detect(Skill):
"""Detect objects of a given class"""
def execute(self, target_class: str) -> List[Detection]: ...
# Composite skills — built from primitives
class Pick(CompositeSkill):
"""Pick = Detect + MoveTo + Grasp"""
def __init__(self, detect: Detect, move: MoveTo, grasp: Grasp):
self.detect = detect
self.move = move
self.grasp = grasp
def execute(self, object_class: str) -> bool:
detections = self.detect.execute(object_class)
if not detections:
return False
approach = compute_approach_pose(detections[0].pose)
if not self.move.execute(approach):
return False
if not self.move.execute(detections[0].pose):
return False
return self.grasp.execute()
class Place(CompositeSkill):
"""Place = MoveTo + Release"""
def __init__(self, move: MoveTo, release: Release):
self.move = move
self.release = release
def execute(self, target: Pose) -> bool:
if not self.move.execute(target):
return False
return self.release.execute()
class PickAndPlace(CompositeSkill):
"""PickAndPlace = Pick + Place — composed from compositions"""
def __init__(self, pick: Pick, place: Place):
self.pick = pick
self.place = place
def execute(self, object_class: str, target: Pose) -> bool:
if not self.pick.execute(object_class):
return False
return self.place.execute(target)
# Dependency injection wires everything together at startup
def build_skill_library(arm, gripper, camera, detector):
move = MoveTo(arm)
grasp = Grasp(gripper)
release = Release(gripper)
look = LookAt(arm)
detect = Detect(camera, detector)
pick = Pick(detect, move, grasp)
place = Place(move, release)
pick_and_place = PickAndPlace(pick, place)
return {
'move': move, 'grasp': grasp, 'release': release,
'pick': pick, 'place': place,
'pick_and_place': pick_and_place,
}
原则 12:优雅降级——利用现有资源继续工作
组件发生故障时,机器人应优雅降级,而不是完全停止。
class DegradedModeManager:
"""Manages capability degradation as components fail"""
def __init__(self):
self.capabilities = {
'full_autonomy': {'requires': ['camera', 'lidar', 'arm', 'gripper']},
'blind_manipulation': {'requires': ['arm', 'gripper']},
'perception_only': {'requires': ['camera', 'lidar']},
'safe_stop': {'requires': []},
}
self.active_components = set()
def component_online(self, name: str):
self.active_components.add(name)
self._update_mode()
def component_offline(self, name: str):
self.active_components.discard(name)
logger.warning(f"Component offline: {name}")
self._update_mode()
def _update_mode(self):
"""Find the best mode we can support with available components"""
for mode, spec in self.capabilities.items():
if set(spec['requires']).issubset(self.active_components):
if mode != self.current_mode:
logger.info(f"Mode change: {self.current_mode} → {mode}")
self.current_mode = mode
return
self.current_mode = 'safe_stop'
self._execute_safe_stop()
快速参考:原则检查清单
代码审查时使用:
| # | 原则 | 检查项 |
|---|---|---|
| 1 | 单一职责 | 能否在不使用“和”的情况下描述该模块? |
| 2 | 依赖倒置 | 高层代码是否导入了硬件驱动? |
| 3 | 开闭原则 | 添加新传感器是否需要修改现有代码? |
| 4 | 接口隔离 | 实现是否被迫为未使用的方法提供空实现? |
| 5 | 里氏替换 | 能否在不更改调用方代码的情况下切换仿真与真实环境? |
| 6 | 速率分离 | 感知是否会阻塞控制循环? |
| 7 | 故障安全默认值 | 通信中断时会发生什么? |
| 8 | 配置优先于代码 | 源代码中是否存在魔法数字? |
| 9 | 幂等操作 | 每条命令发送两次是否安全? |
| 10 | 全面观测 | 仅凭日志能否诊断凌晨 2 点发生的故障? |
| 11 | 可组合性 | 能否利用现有技能构建新任务? |
| 12 | 优雅降级 | 传感器发生故障时,机器人的行为是什么? |