mcpField
R
robotics-design-patterns

robotics-design-patterns

github.com/arpitg1304開発386258 回インストール11 週間前に更新
リポジトリ

robotics-design-patterns について

ロボットシステムの設計者向けに、設計パターンをまとめたガイドです。ビヘイビアツリー、状態機械、知覚パイプライン、ハードウェア抽象化に加え、安全性やシム・トゥ・リアル、リアルタイム設計も扱います。

主な機能

  1. 01ロボットソフトウェアの階層アーキテクチャ
  2. 02ビヘイビアツリーと FSM の選択
  3. 03知覚、計画、制御パイプラインの設計
  4. 04ハードウェア抽象化とシム・トゥ・リアル設計
  5. 05安全監視と作業空間の制限
  6. 06データ記録とリアルタイム設計の判断

活用シーン

  1. 01新しいロボットのソフトウェア構成を計画
  2. 02ビヘイビアツリーと FSM の選択
  3. 03シミュレーションと実機で共用するインターフェース設計
  4. 04システムへのウォッチドッグと安全境界の追加

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/arpitg1304/robotics-agent-skills --skill robotics-design-patterns

インストール後、Claude Code で/robotics-design-patternsを入力して読み込まれたか確認します。

詳細

ロボティクスの設計パターン

このスキルを使う場面

  • ロボットソフトウェアのアーキテクチャをゼロから設計するとき
  • ビヘイビアツリー、FSM、ハイブリッド方式のいずれを選ぶか検討するとき
  • 知覚 → 計画 → 制御のパイプラインを構成するとき
  • 安全システムやウォッチドッグを実装するとき
  • ハードウェア抽象化レイヤー(HAL)を構築するとき
  • シミュレーションから実機への移行を考慮して設計するとき
  • 複数ロボット/フリートシステムを構築するとき
  • リアルタイムと非リアルタイムのトレードオフを検討するとき

パターン 1:ロボットソフトウェアスタック

複雑さにかかわらず、すべてのロボットシステムは次の階層型アーキテクチャに従います。

┌─────────────────────────────────────────────┐
│               APPLICATION LAYER              │
│    Mission planning, task allocation, UI     │
├─────────────────────────────────────────────┤
│              BEHAVIORAL LAYER                │
│  Behavior trees, FSMs, decision-making       │
├─────────────────────────────────────────────┤
│             FUNCTIONAL LAYER                 │
│  Perception, Planning, Control, Estimation   │
├─────────────────────────────────────────────┤
│           COMMUNICATION LAYER                │
│     ROS2, DDS, shared memory, IPC            │
├─────────────────────────────────────────────┤
│          HARDWARE ABSTRACTION LAYER          │
│    Drivers, sensor interfaces, actuators     │
├─────────────────────────────────────────────┤
│              HARDWARE LAYER                  │
│    Cameras, LiDARs, motors, grippers, IMUs   │
└─────────────────────────────────────────────┘

設計ルール:情報は知覚を通じて上位へ流れ、判断は制御を通じて下位へ流れます。アプリケーション層からハードウェアへ直接指示を出してはいけません。

パターン 2:ビヘイビアツリー(BT)

ビヘイビアツリーは、ロボットの意思決定における推奨されるデフォルトです。モジュール化と再利用がしやすく、複雑な動作ではFSMよりデバッグも容易です。

コアノードの種類
Sequence (→)     : Execute children left-to-right, FAIL on first failure
Fallback (?)     : Execute children left-to-right, SUCCEED on first success
Parallel (⇉)     : Execute all children simultaneously
Decorator        : Modify a single child's behavior
Action (leaf)    : Execute a robot action
Condition (leaf) : Check a condition (no side effects)
例:ピックアンドプレースのBT
                    → Sequence
                   /    |      \
            → Check     → Pick     → Place
           /    \      /   |  \     /  |  \
       Battery  Obj  Open  Move  Close Move Open Release
       OK?    Found? Grip  To    Grip  To   Grip
                      per  Obj   per   Goal per
実装パターン
import py_trees

class MoveToTarget(py_trees.behaviour.Behaviour):
    """Action node: Move robot to a target pose"""

    def __init__(self, name, target_key="target_pose"):
        super().__init__(name)
        self.target_key = target_key
        self.action_client = None

    def setup(self, **kwargs):
        """Called once when tree is set up — initialize resources"""
        self.node = kwargs.get('node')  # ROS2 node
        self.action_client = ActionClient(
            self.node, MoveBase, 'move_base')

    def initialise(self):
        """Called when this node first ticks — send the goal"""
        bb = self.blackboard
        target = bb.get(self.target_key)
        self.goal_handle = self.action_client.send_goal(target)
        self.logger.info(f"Moving to {target}")

    def update(self):
        """Called every tick — check progress"""
        if self.goal_handle is None:
            return py_trees.common.Status.FAILURE

        status = self.goal_handle.status
        if status == GoalStatus.STATUS_SUCCEEDED:
            return py_trees.common.Status.SUCCESS
        elif status == GoalStatus.STATUS_ABORTED:
            return py_trees.common.Status.FAILURE
        else:
            return py_trees.common.Status.RUNNING

    def terminate(self, new_status):
        """Called when node exits — cancel if preempted"""
        if new_status == py_trees.common.Status.INVALID:
            if self.goal_handle:
                self.goal_handle.cancel_goal()
                self.logger.info("Movement cancelled")

# Build the tree
def create_pick_place_tree():
    root = py_trees.composites.Sequence("PickAndPlace", memory=True)

    # Safety checks (Fallback: if any fails, abort)
    safety = py_trees.composites.Sequence("SafetyChecks", memory=False)
    safety.add_children([
        CheckBattery("BatteryOK", threshold=20.0),
        CheckEStop("EStopClear"),
    ])

    pick = py_trees.composites.Sequence("Pick", memory=True)
    pick.add_children([
        DetectObject("FindObject"),
        MoveToTarget("ApproachObject", target_key="object_pose"),
        GripperCommand("CloseGripper", action="close"),
    ])

    place = py_trees.composites.Sequence("Place", memory=True)
    place.add_children([
        MoveToTarget("MoveToPlace", target_key="place_pose"),
        GripperCommand("OpenGripper", action="open"),
    ])

    root.add_children([safety, pick, place])
    return root
ブラックボードパターン
# The Blackboard is the shared memory for BT nodes
bb = py_trees.blackboard.Blackboard()

# Perception nodes WRITE to blackboard
class DetectObject(py_trees.behaviour.Behaviour):
    def update(self):
        detections = self.perception.detect()
        if detections:
            self.blackboard.set("object_pose", detections[0].pose)
            self.blackboard.set("object_class", detections[0].label)
            return Status.SUCCESS
        return Status.FAILURE

# Action nodes READ from blackboard
class MoveToTarget(py_trees.behaviour.Behaviour):
    def initialise(self):
        target = self.blackboard.get("object_pose")
        self.send_goal(target)

パターン 3:有限状態機械(FSM)

明確な状態を持つ、単純で明確に定義された逐次動作にはFSMを使います。複雑な動作にはBTを優先してください。

from enum import Enum, auto
import smach  # ROS state machine library

class RobotState(Enum):
    IDLE = auto()
    NAVIGATING = auto()
    PICKING = auto()
    PLACING = auto()
    ERROR = auto()
    CHARGING = auto()

# SMACH implementation
class NavigateState(smach.State):
    def __init__(self):
        smach.State.__init__(self,
            outcomes=['succeeded', 'aborted', 'preempted'],
            input_keys=['target_pose'],
            output_keys=['final_pose'])

    def execute(self, userdata):
        # Navigation logic
        result = navigate_to(userdata.target_pose)
        if result.success:
            userdata.final_pose = result.pose
            return 'succeeded'
        return 'aborted'

# Build state machine
sm = smach.StateMachine(outcomes=['done', 'failed'])
with sm:
    smach.StateMachine.add('NAVIGATE', NavigateState(),
        transitions={'succeeded': 'PICK', 'aborted': 'ERROR'})
    smach.StateMachine.add('PICK', PickState(),
        transitions={'succeeded': 'PLACE', 'aborted': 'ERROR'})
    smach.StateMachine.add('PLACE', PlaceState(),
        transitions={'succeeded': 'done', 'aborted': 'ERROR'})
    smach.StateMachine.add('ERROR', ErrorRecovery(),
        transitions={'recovered': 'NAVIGATE', 'fatal': 'failed'})

FSMとBTの使い分け:

  • FSM:線形のワークフロー、単純なデバイス、UIの状態、プロトコルの実装
  • BT:複雑なロボット、リアクティブな動作、多数の条件分岐、再利用可能なサブ動作

パターン 4:知覚パイプライン

Raw Sensors → Preprocessing → Detection/Estimation → Fusion → World Model
センサーフュージョンアーキテクチャ
class SensorFusion:
    """Multi-sensor fusion using a central world model"""

    def __init__(self):
        self.world_model = WorldModel()
        self.filters = {
            'pose': ExtendedKalmanFilter(state_dim=6),
            'objects': MultiObjectTracker(),
        }

    def update_from_camera(self, detections, timestamp):
        """Camera provides object detections with high latency"""
        for det in detections:
            self.filters['objects'].update(
                det, sensor='camera',
                uncertainty=det.confidence,
                timestamp=timestamp
            )

    def update_from_lidar(self, points, timestamp):
        """LiDAR provides precise geometry with lower latency"""
        clusters = self.segment_points(points)
        for cluster in clusters:
            self.filters['objects'].update(
                cluster, sensor='lidar',
                uncertainty=0.02,  # 2cm typical LiDAR accuracy
                timestamp=timestamp
            )

    def update_from_imu(self, imu_data, timestamp):
        """IMU provides high-frequency attitude estimates"""
        self.filters['pose'].predict(imu_data, dt=timestamp - self.last_imu_t)
        self.last_imu_t = timestamp

    def get_world_state(self):
        """Query the fused world model"""
        return WorldState(
            robot_pose=self.filters['pose'].state,
            objects=self.filters['objects'].get_tracked_objects(),
            confidence=self.filters['objects'].get_confidence_map()
        )
知覚と動作のループのタイミング
Camera (30Hz)  ─┐
LiDAR (10Hz)   ─┼──→ Fusion (50Hz) ──→ Planner (10Hz) ──→ Controller (100Hz+)
IMU (200Hz)    ─┘

RULE: Controller frequency > Planner frequency > Sensor frequency
      This ensures smooth execution despite variable perception latency.

パターン 5:ハードウェア抽象化レイヤー(HAL)

**アプリケーションコードからハードウェアへ直接アクセスさせてはいけません。**必ず抽象化レイヤーを経由させてください。

from abc import ABC, abstractmethod

class GripperInterface(ABC):
    """Abstract gripper interface — implement for each hardware type"""

    @abstractmethod
    def open(self, width: float = 1.0) -> bool: ...

    @abstractmethod
    def close(self, force: float = 0.5) -> bool: ...

    @abstractmethod
    def get_state(self) -> GripperState: ...

    @abstractmethod
    def get_width(self) -> float: ...


class RobotiqGripper(GripperInterface):
    """Concrete implementation for Robotiq 2F-85"""
    def __init__(self, port='/dev/ttyUSB0'):
        self.serial = serial.Serial(port, 115200)
        # ... Modbus RTU setup

    def close(self, force=0.5):
        cmd = self._build_modbus_cmd(force=int(force * 255))
        self.serial.write(cmd)
        return self._wait_for_completion()


class SimulatedGripper(GripperInterface):
    """Simulation gripper for testing"""
    def __init__(self):
        self.width = 0.085  # 85mm open
        self.state = GripperState.OPEN

    def close(self, force=0.5):
        self.width = 0.0
        self.state = GripperState.CLOSED
        return True


# Factory pattern for hardware instantiation
def create_gripper(config: dict) -> GripperInterface:
    gripper_type = config.get('type', 'simulated')
    if gripper_type == 'robotiq':
        return RobotiqGripper(port=config['port'])
    elif gripper_type == 'simulated':
        return SimulatedGripper()
    else:
        raise ValueError(f"Unknown gripper type: {gripper_type}")

パターン 6:安全システム

安全階層
Level 0: Hardware E-Stop (physical button, cuts power)
Level 1: Safety-rated controller (SIL2/SIL3, hardware watchdog)
Level 2: Software watchdog (monitors heartbeats, enforces limits)
Level 3: Application safety (collision avoidance, workspace limits)
ソフトウェアウォッチドッグパターン
import threading
import time

class SafetyWatchdog:
    """Monitors system health and triggers safe stop on failures"""

    def __init__(self, timeout_ms=500):
        self.timeout = timeout_ms / 1000.0
        self.heartbeats = {}
        self.lock = threading.Lock()
        self.safe_stop_triggered = False

        # Start monitoring thread
        self.monitor_thread = threading.Thread(
            target=self._monitor_loop, daemon=True)
        self.monitor_thread.start()

    def register_component(self, name: str, critical: bool = True):
        """Register a component that must send heartbeats"""
        with self.lock:
            self.heartbeats[name] = {
                'last_beat': time.monotonic(),
                'critical': critical,
                'alive': True
            }

    def heartbeat(self, name: str):
        """Called by components to signal they're alive"""
        with self.lock:
            if name in self.heartbeats:
                self.heartbeats[name]['last_beat'] = time.monotonic()
                self.heartbeats[name]['alive'] = True

    def _monitor_loop(self):
        while True:
            now = time.monotonic()
            with self.lock:
                for name, info in self.heartbeats.items():
                    elapsed = now - info['last_beat']
                    if elapsed > self.timeout and info['alive']:
                        info['alive'] = False
                        if info['critical']:
                            self._trigger_safe_stop(
                                f"Critical component '{name}' "
                                f"timed out ({elapsed:.1f}s)")
            time.sleep(self.timeout / 4)

    def _trigger_safe_stop(self, reason: str):
        if not self.safe_stop_triggered:
            self.safe_stop_triggered = True
            logger.critical(f"SAFE STOP: {reason}")
            self._execute_safe_stop()

    def _execute_safe_stop(self):
        """Bring robot to a safe state"""
        # 1. Stop all motion (zero velocity command)
        # 2. Engage brakes
        # 3. Publish emergency state to all nodes
        # 4. Log the event
        pass
作業空間の制限
class WorkspaceMonitor:
    """Enforce that robot stays within safe operational bounds"""

    def __init__(self, limits: dict):
        self.joint_limits = limits['joints']    # {joint: (min, max)}
        self.cartesian_bounds = limits['cartesian']  # AABB or convex hull
        self.velocity_limits = limits['velocity']
        self.force_limits = limits['force']

    def check_command(self, command) -> SafetyResult:
        """Validate a command BEFORE sending to hardware"""
        violations = []

        # Joint limit check
        for joint, value in command.joint_positions.items():
            lo, hi = self.joint_limits[joint]
            if not (lo <= value <= hi):
                violations.append(
                    f"Joint {joint}={value:.3f} outside [{lo:.3f}, {hi:.3f}]")

        # Velocity check
        for joint, vel in command.joint_velocities.items():
            if abs(vel) > self.velocity_limits[joint]:
                violations.append(
                    f"Joint {joint} velocity {vel:.3f} exceeds limit")

        if violations:
            return SafetyResult(safe=False, violations=violations)
        return SafetyResult(safe=True)

パターン 7:シミュレーションから実機への移行アーキテクチャ

┌────────────────────────────────────┐
│         Application Code           │
│  (Same code runs in sim AND real)  │
├──────────────┬─────────────────────┤
│   Sim HAL    │     Real HAL        │
│  (MuJoCo/    │  (Hardware          │
│   Gazebo/    │   drivers)          │
│   Isaac)     │                     │
└──────────────┴─────────────────────┘

主な原則:

  1. アプリケーションコードは、シミュレーション環境か実機環境かを決して認識しません
  2. 同じメッセージ型、同じトピック名、同じインターフェースを使います
  3. use_sim_time パラメーターを使ってクロックソースを切り替えます
  4. ドメインランダム化はシミュレーション用HALの内部で行います
  5. 転移学習アダプターはHALの境界に配置します
# Config-driven sim/real switching
class RobotDriver:
    def __init__(self, config):
        if config['mode'] == 'simulation':
            self.arm = SimulatedArm(config['sim'])
            self.camera = SimulatedCamera(config['sim'])
        elif config['mode'] == 'real':
            self.arm = UR5Driver(config['real']['arm_ip'])
            self.camera = RealSenseDriver(config['real']['camera_serial'])

        # Application code uses the same interface regardless
        self.perception = PerceptionPipeline(self.camera)
        self.planner = MotionPlanner(self.arm)

パターン 8:データ記録アーキテクチャ

学習ベースのロボティクスでは不可欠です — ForgeIRエコシステム向けに設計されています:

┌─────────────────────────────────────────────┐
│              Event-Based Recorder            │
│  Triggers: action boundaries, anomalies,     │
│  task completions, operator signals           │
├─────────────────────────────────────────────┤
│           Multimodal Data Streams            │
│  Camera (30Hz) | Joint State (100Hz) |       │
│  Force/Torque (1kHz) | Language Annotations  │
├─────────────────────────────────────────────┤
│            Storage Layer                     │
│  Episode-based structure with metadata       │
│  Format: MCAP / Zarr / HDF5 / RLDS          │
├─────────────────────────────────────────────┤
│           Quality Assessment                 │
│  Completeness checks, trajectory validation  │
│  Anomaly detection, diversity analysis       │
└─────────────────────────────────────────────┘
class EpisodeRecorder:
    """Records robot episodes with event-based boundaries"""

    def __init__(self, config):
        self.streams = {}
        self.episode_active = False
        self.current_episode = None
        self.storage = StorageBackend(config['format'])  # Zarr, MCAP, etc.

    def register_stream(self, name, msg_type, frequency_hz):
        self.streams[name] = StreamConfig(
            name=name, type=msg_type, freq=frequency_hz)

    def start_episode(self, metadata: dict):
        """Begin recording an episode with metadata"""
        self.current_episode = Episode(
            id=uuid4(),
            start_time=time.monotonic(),
            metadata=metadata,  # task, operator, environment, etc.
            streams={name: [] for name in self.streams}
        )
        self.episode_active = True

    def record_step(self, stream_name, data, timestamp):
        if self.episode_active:
            self.current_episode.streams[stream_name].append(
                DataPoint(data=data, timestamp=timestamp))

    def end_episode(self, outcome: str, annotations: dict = None):
        """Finalize and store the episode"""
        self.episode_active = False
        self.current_episode.end_time = time.monotonic()
        self.current_episode.outcome = outcome
        self.current_episode.annotations = annotations

        # Validate before saving
        quality = self.validate_episode(self.current_episode)
        self.current_episode.quality_score = quality

        self.storage.save(self.current_episode)
        return self.current_episode.id

避けるべきアンチパターン

1. 神ノード

問題:1つのノードが知覚、計画、制御、ログ記録のすべてを担います。 対策:単一責任にします。1つのノードに1つの役割を持たせ、トピック経由で接続します。

2. マジックナンバーのハードコード

問題:if distance < 0.35: があちこちに散在します。 対策:説明的な名前のパラメーターを使い、設定ファイルから読み込みます。

3. イベントではなくポーリングを使う

問題:while True: check_sensor(); sleep(0.01) 対策:コールバック、サブスクライバー、イベント駆動型アーキテクチャを使います。

4. エラー復旧がない

問題:最初のエラーでロボットが停止したままになります。 対策:すべてのアクションノードに失敗時の動作が必要です。フォールバックを備えたビヘイビアツリーを使います。

5. シミュレーション専用コード

問題:シミュレーションでは完璧に動くコードが、実機ではクラッシュします。 対策:HALパターンを使います。早い段階から頻繁にハードウェア・イン・ザ・ループでテストします。

6. タイムスタンプがない

問題:タイムスタンプのないセンサーデータは、フュージョンも再生もできません。 対策:ソースで必ずすべてにタイムスタンプを付けます。制御には単調クロックを使います。

7. 制御ループをブロックする

問題:知覚処理が100Hzの制御ループをブロックします。 対策:プロセス/スレッドを分離します。制御ループをブロックしてはいけません。

8. データを記録しない

問題:バグを再現できず、モデルをトレーニングできず、動作を監査できません。 対策:常に記録します。イベントベースの記録は低コストです。MCAP形式を使います。

アーキテクチャ決定チェックリスト

新しいロボットシステムを設計する際は、次の質問に答えてください。

  1. 安全アーキテクチャはどうなっていますか?(非常停止、ウォッチドッグ、作業空間の制限)
  2. リアルタイム要件は何ですか?(制御は100Hz以上、知覚は10~30Hz)
  3. 動作フレームワークは何ですか?(複雑な場合はBT、単純な場合はFSM)
  4. シミュレーションから実機への移行はどのように行いますか?(HALパターン、同じインターフェース)
  5. データはどのように記録しますか?(エピソード単位、イベントトリガー方式、メタデータ付き)
  6. 障害にはどのように対処しますか?(段階的な機能縮退、復旧動作)
  7. 通信ミドルウェアは何ですか?(多くの場合はROS2)
  8. システムはどのようにデプロイしますか?(Docker、snap、直接インストール)
  9. どのようにテストしますか?(単体テスト、統合テスト、ハードウェア・イン・ザ・ループテスト、実地テスト)
  10. どのように監視しますか?(ハートビート、メトリクス、ダッシュボード)

コメント