397.82 SMACH 기반 상태 머신 계획 도구

1. 개요

SMACH(State MAChine)는 ROS(Robot Operating System) 생태계에서 로봇의 고수준 임무 실행 흐름을 유한 상태 머신(Finite State Machine, FSM) 패러다임으로 구성하기 위한 Python 기반 라이브러리이다. SMACH는 복잡한 로봇 임무를 이산적 상태(Discrete State)의 전이(Transition)로 모델링하여, 임무 실행의 논리적 흐름을 명확하고 체계적으로 정의할 수 있는 프레임워크를 제공한다.

SMACH는 원래 Willow Garage의 PR2 로봇 프로젝트를 위해 개발되었으며(Bohren & Cousins, 2010), 이후 ROS 커뮤니티에서 널리 채택되어 다양한 자율 로봇 시스템의 임무 계획 및 실행 도구로 활용되고 있다. SMACH의 핵심 설계 철학은 로봇 임무의 복잡한 실행 로직을 직관적이면서도 형식적으로 관리 가능한 상태 머신 구조로 추상화하는 것이다.

2. 유한 상태 머신의 이론적 기초

2.1 형식적 정의

유한 상태 머신은 수학적으로 5-튜플(5-tuple) M = (S, \Sigma, \delta, s_0, F)로 정의된다:

  • S: 유한 상태 집합(Finite Set of States)
  • \Sigma: 입력 알파벳(Input Alphabet), 이벤트 집합에 해당한다
  • \delta: S \times \Sigma \rightarrow S: 상태 전이 함수(Transition Function)
  • s_0 \in S: 초기 상태(Initial State)
  • F \subseteq S: 최종 상태 집합(Set of Final States), 종료 결과(Outcome)에 해당한다

SMACH에서는 이 형식적 모델을 로봇 임무 맥락에 적응시켜, 각 상태가 로봇의 특정 행위(Action)나 조건 평가(Condition Evaluation)를 수행하고, 상태의 결과(Outcome)에 따라 다음 상태로 전이하는 반응적 실행 모델을 구현한다.

2.2 확장된 상태 머신 모델

전통적 유한 상태 머신은 내부 데이터의 관리가 제한적이다. SMACH는 이를 확장하여, 상태 간에 공유 데이터를 전달할 수 있는 사용자 데이터(UserData) 메커니즘을 도입한다. 이는 확장된 상태 머신(Extended State Machine) 또는 확장된 유한 오토마톤(Extended Finite Automaton) 모델에 해당하며, 상태 전이 시 데이터 변환 및 전파를 지원한다.

3. SMACH의 아키텍처

3.1 핵심 추상화 계층

SMACH의 아키텍처는 세 개의 핵심 추상화 계층으로 구성된다:

상태(State) 계층: 가장 기본적인 실행 단위로서, smach.State 클래스를 상속하여 사용자가 정의한다. 각 상태는 execute() 메서드를 구현하며, 이 메서드의 반환값이 해당 상태의 결과(Outcome)가 된다. 결과는 문자열(String)으로 표현되며, 상태 전이의 트리거 역할을 한다.

컨테이너(Container) 계층: 복수의 상태를 논리적으로 그룹화하는 구조적 단위이다. 컨테이너 자체도 상태 인터페이스를 구현하므로, 컨테이너의 중첩(Nesting)을 통해 계층적 상태 머신(Hierarchical State Machine, HSM)을 구성할 수 있다.

사용자 데이터(UserData) 계층: 상태 간 데이터 흐름을 관리하는 메커니즘으로, 입력 키(Input Key)와 출력 키(Output Key)를 통해 상태 간 데이터의 명시적 전달을 보장한다.

3.2 상태 정의와 구현

SMACH에서 개별 상태의 구현은 smach.State 클래스의 하위 클래스를 정의하는 방식으로 이루어진다:

import smach
import rospy

class NavigateToWaypoint(smach.State):
    def __init__(self):
        smach.State.__init__(
            self,
            outcomes=['succeeded', 'failed', 'preempted'],
            input_keys=['target_waypoint'],
            output_keys=['navigation_result']
        )

    def execute(self, userdata):
        rospy.loginfo(f'Navigating to {userdata.target_waypoint}')

        # 내비게이션 액션 클라이언트를 통한 목표 지점 이동
        goal = MoveBaseGoal()
        goal.target_pose = userdata.target_waypoint

        self.navigation_client.send_goal(goal)
        self.navigation_client.wait_for_result()

        result = self.navigation_client.get_state()
        if result == GoalStatus.SUCCEEDED:
            userdata.navigation_result = 'success'
            return 'succeeded'
        elif result == GoalStatus.PREEMPTED:
            return 'preempted'
        else:
            userdata.navigation_result = 'failure'
            return 'failed'

이 구현에서 outcomes는 상태가 반환할 수 있는 결과의 집합을 정의하고, input_keysoutput_keys는 상태 간에 전달되는 데이터의 인터페이스를 명시한다. execute() 메서드는 상태의 실제 행위를 구현하며, 반환값으로 결과 문자열을 생성한다.

4. 컨테이너 유형

4.1 상태 머신 컨테이너(StateMachine Container)

상태 머신 컨테이너는 SMACH의 가장 기본적인 컨테이너 유형으로, 복수의 상태와 이들 간의 전이를 정의한다:

import smach

def create_patrol_sm():
    sm = smach.StateMachine(outcomes=['mission_complete', 'mission_failed'])

    sm.userdata.waypoints = [wp1, wp2, wp3, wp4]
    sm.userdata.current_index = 0

    with sm:
        smach.StateMachine.add(
            'NAVIGATE',
            NavigateToWaypoint(),
            transitions={
                'succeeded': 'INSPECT',
                'failed': 'HANDLE_NAV_ERROR',
                'preempted': 'mission_failed'
            },
            remapping={
                'target_waypoint': 'current_waypoint'
            }
        )

        smach.StateMachine.add(
            'INSPECT',
            PerformInspection(),
            transitions={
                'succeeded': 'SELECT_NEXT',
                'failed': 'HANDLE_INSPECT_ERROR',
                'preempted': 'mission_failed'
            }
        )

        smach.StateMachine.add(
            'SELECT_NEXT',
            SelectNextWaypoint(),
            transitions={
                'next_available': 'NAVIGATE',
                'all_visited': 'mission_complete'
            }
        )

        smach.StateMachine.add(
            'HANDLE_NAV_ERROR',
            NavigationErrorHandler(),
            transitions={
                'retry': 'NAVIGATE',
                'abort': 'mission_failed'
            }
        )

        smach.StateMachine.add(
            'HANDLE_INSPECT_ERROR',
            InspectionErrorHandler(),
            transitions={
                'retry': 'INSPECT',
                'skip': 'SELECT_NEXT',
                'abort': 'mission_failed'
            }
        )

    return sm

이 예시는 순찰(Patrol) 임무를 위한 상태 머신을 구성한다. NAVIGATEINSPECTSELECT_NEXT의 주요 흐름과 함께, 각 단계에서 발생할 수 있는 오류를 처리하는 HANDLE_NAV_ERRORHANDLE_INSPECT_ERROR 상태를 포함한다. 상태 전이 맵(Transition Map)은 각 상태의 결과를 다음 상태에 매핑하여 임무의 전체 흐름을 정의한다.

4.2 동시성 컨테이너(Concurrence Container)

동시성 컨테이너는 복수의 상태를 병렬로 실행하고, 개별 상태의 결과를 조합하여 컨테이너의 최종 결과를 결정한다:

def create_concurrent_monitoring():
    cc = smach.Concurrence(
        outcomes=['monitoring_complete', 'anomaly_detected', 'preempted'],
        default_outcome='monitoring_complete',
        outcome_map={
            'anomaly_detected': {
                'SENSOR_MONITOR': 'anomaly',
                'HEALTH_MONITOR': 'critical'
            }
        },
        child_termination_cb=child_term_callback,
        outcome_cb=outcome_callback
    )

    with cc:
        smach.Concurrence.add('NAVIGATE_PATROL', PatrolNavigation())
        smach.Concurrence.add('SENSOR_MONITOR', SensorDataMonitor())
        smach.Concurrence.add('HEALTH_MONITOR', SystemHealthMonitor())
        smach.Concurrence.add('COMM_MONITOR', CommunicationMonitor())

    return cc

동시성 컨테이너의 outcome_map은 자식 상태들의 결과 조합에 따라 컨테이너의 결과를 결정하는 규칙을 정의한다. child_termination_cb은 하나의 자식 상태가 종료되었을 때 다른 자식 상태들의 선점(Preemption) 여부를 결정하는 콜백 함수이다.

4.3 시퀀스 컨테이너(Sequence Container)

시퀀스 컨테이너는 상태들의 순차적 실행을 간결하게 정의하기 위한 편의 컨테이너이다. 각 상태의 기본 결과(Default Outcome)가 자동으로 다음 상태로의 전이에 매핑되며, 명시적 전이 정의의 중복을 줄인다:

def create_pickup_sequence():
    sq = smach.Sequence(
        outcomes=['completed', 'failed'],
        connector_outcome='succeeded'
    )

    with sq:
        smach.Sequence.add('APPROACH_OBJECT', ApproachObject())
        smach.Sequence.add('ALIGN_GRIPPER', AlignGripper())
        smach.Sequence.add('GRASP_OBJECT', GraspObject())
        smach.Sequence.add('LIFT_OBJECT', LiftObject())
        smach.Sequence.add('VERIFY_GRASP', VerifyGrasp())

    return sq

connector_outcome으로 지정된 결과(‘succeeded’)가 반환되면 자동으로 시퀀스의 다음 상태로 전이하며, 마지막 상태의 해당 결과는 시퀀스 컨테이너의 ‘completed’ 결과로 매핑된다.

5. 계층적 상태 머신 구성

5.1 중첩 구조를 통한 모듈화

SMACH의 강력한 기능 중 하나는 컨테이너 자체가 상태 인터페이스를 구현하므로, 상태 머신 내에 또 다른 상태 머신을 중첩할 수 있다는 점이다. 이를 통해 복잡한 임무를 계층적으로 분해(Hierarchical Decomposition)하여 관리할 수 있다:

def create_mission_sm():
    # 최상위 임무 상태 머신
    mission_sm = smach.StateMachine(
        outcomes=['mission_success', 'mission_failure']
    )

    with mission_sm:
        # 초기화 서브 상태 머신
        init_sm = create_initialization_sm()
        smach.StateMachine.add(
            'INITIALIZATION',
            init_sm,
            transitions={
                'init_complete': 'MAIN_OPERATION',
                'init_failed': 'SAFE_SHUTDOWN'
            }
        )

        # 주 작전 서브 상태 머신 (동시성 컨테이너 포함)
        main_sm = create_main_operation_sm()
        smach.StateMachine.add(
            'MAIN_OPERATION',
            main_sm,
            transitions={
                'operation_complete': 'RETURN_TO_BASE',
                'critical_failure': 'EMERGENCY_LANDING'
            }
        )

        # 귀환 서브 상태 머신
        return_sm = create_return_sm()
        smach.StateMachine.add(
            'RETURN_TO_BASE',
            return_sm,
            transitions={
                'returned': 'FINALIZATION',
                'return_failed': 'EMERGENCY_LANDING'
            }
        )

        smach.StateMachine.add(
            'FINALIZATION',
            FinalizeMission(),
            transitions={
                'succeeded': 'mission_success',
                'failed': 'mission_failure'
            }
        )

        smach.StateMachine.add(
            'EMERGENCY_LANDING',
            EmergencyLanding(),
            transitions={
                'landed': 'mission_failure'
            }
        )

        smach.StateMachine.add(
            'SAFE_SHUTDOWN',
            SafeShutdown(),
            transitions={
                'shutdown_complete': 'mission_failure'
            }
        )

    return mission_sm

이러한 계층적 구성은 각 서브 임무를 독립적인 모듈로 개발하고 테스트할 수 있게 하며, 임무의 전체 구조를 상위 수준에서 파악할 수 있도록 한다. 계층적 상태 머신은 계층적 작업 네트워크(HTN)의 과업 분해와 유사한 구조적 분할 원리를 임무 실행 수준에서 구현한 것으로 볼 수 있다.

5.2 사용자 데이터의 계층적 전파

중첩된 상태 머신 구조에서 사용자 데이터의 전파는 remapping 메커니즘을 통해 이루어진다. 부모 컨테이너와 자식 컨테이너 사이의 데이터 흐름은 명시적으로 정의되며, 이를 통해 데이터 의존성(Data Dependency)의 투명성을 보장한다.

부모 상태 머신의 사용자 데이터 키와 자식 상태 머신의 사용자 데이터 키 간의 매핑을 remapping 사전(Dictionary)으로 지정하며, 이는 컴파일 타임이 아닌 런타임에 검증된다. 키 매핑의 오류는 실행 시점에 예외(Exception)로 보고되므로, 개발 단계에서 상태 머신의 사전 검증(Pre-validation)을 수행하는 것이 권장된다.

6. ROS 통합 인터페이스

6.1 ActionState를 통한 액션 서버 통합

SMACH는 ROS의 액션 서버(Action Server)와의 통합을 위해 SimpleActionState를 제공한다. 이를 통해 ROS 액션을 SMACH 상태로 직접 래핑(Wrapping)할 수 있다:

from smach_ros import SimpleActionState
from move_base_msgs.msg import MoveBaseAction, MoveBaseGoal

def create_navigation_state(target_pose):
    goal = MoveBaseGoal()
    goal.target_pose = target_pose

    return SimpleActionState(
        'move_base',
        MoveBaseAction,
        goal=goal,
        result_slots=['base_position'],
        outcomes=['succeeded', 'aborted', 'preempted']
    )

SimpleActionState는 액션 목표(Goal)의 전송, 피드백(Feedback) 수신, 결과(Result) 처리를 자동으로 관리하며, 액션 서버의 완료 상태를 SMACH 결과로 변환한다.

6.2 ServiceState를 통한 서비스 통합

ROS 서비스(Service) 호출을 SMACH 상태로 통합하기 위해 ServiceState가 제공된다:

from smach_ros import ServiceState
from std_srvs.srv import SetBool, SetBoolRequest

arm_state = ServiceState(
    'arm_motors',
    SetBool,
    request=SetBoolRequest(data=True),
    response_slots=['success', 'message'],
    outcomes=['succeeded', 'failed']
)

6.3 MonitorState를 통한 토픽 모니터링

MonitorState는 ROS 토픽을 구독(Subscribe)하여 특정 조건이 충족될 때까지 대기하는 상태를 구현한다:

from smach_ros import MonitorState
from sensor_msgs.msg import BatteryState

def battery_check_cb(userdata, msg):
    if msg.percentage < 0.2:
        return False  # 조건 불충족, 상태 종료
    return True  # 조건 충족, 모니터링 계속

battery_monitor = MonitorState(
    'battery_state',
    BatteryState,
    cond_cb=battery_check_cb,
    max_checks=-1  # 무한 모니터링
)

이러한 ROS 통합 상태들을 통해, SMACH 상태 머신은 ROS 생태계의 다양한 기능 모듈과 원활하게 연동될 수 있다.

7. SMACH Viewer를 통한 시각화 및 디버깅

7.1 실시간 상태 시각화

SMACH는 smach_viewer라는 전용 시각화 도구를 제공한다. 이 도구는 실행 중인 상태 머신의 구조와 현재 활성 상태를 실시간으로 그래프 형태로 표시한다. 시각화에는 다음의 정보가 포함된다:

  • 상태 노드: 각 상태가 노드로 표시되며, 현재 활성 상태는 색상으로 강조된다.
  • 전이 간선: 상태 간 전이가 방향성 간선으로 표시되며, 간선에는 전이를 트리거하는 결과 문자열이 라벨로 부착된다.
  • 계층 구조: 중첩된 컨테이너는 확장 가능한 서브 그래프로 표시되어, 계층적 구조의 탐색이 가능하다.
  • 사용자 데이터: 현재 상태의 사용자 데이터 값을 패널에 표시하여, 데이터 흐름의 추적을 지원한다.

7.2 내성(Introspection) 서버

SMACH의 시각화는 내성 서버(Introspection Server)를 통해 구현된다. 내성 서버는 상태 머신의 구조 정보와 실행 상태를 ROS 토픽으로 발행(Publish)하여, smach_viewer나 사용자 정의 모니터링 도구가 이를 구독할 수 있도록 한다:

import smach_ros

# 상태 머신 생성
sm = create_mission_sm()

# 내성 서버 생성 및 시작
sis = smach_ros.IntrospectionServer(
    'mission_introspection',
    sm,
    '/SM_ROOT'
)
sis.start()

# 상태 머신 실행
outcome = sm.execute()

# 내성 서버 종료
sis.stop()

8. 선점(Preemption) 메커니즘

8.1 선점의 필요성과 구현

자율 로봇 시스템에서 임무의 동적 중단 및 전환은 필수적인 기능이다. SMACH는 모든 상태와 컨테이너에 선점 메커니즘을 내장하고 있다. 선점 요청이 수신되면, 현재 실행 중인 상태의 request_preempt() 메서드가 호출되고, 상태는 안전하게 실행을 종료한 후 ‘preempted’ 결과를 반환한다.

선점의 전파는 계층적으로 이루어진다. 최상위 상태 머신에 대한 선점 요청은 현재 활성화된 자식 상태에 재귀적으로 전파되며, 최하위 실행 단위에서부터 상위로 순차적으로 종료가 이루어진다.

class LongRunningTask(smach.State):
    def __init__(self):
        smach.State.__init__(
            self,
            outcomes=['succeeded', 'failed', 'preempted']
        )

    def execute(self, userdata):
        rate = rospy.Rate(10)
        while not rospy.is_shutdown():
            # 선점 확인
            if self.preempt_requested():
                self.service_preempt()
                return 'preempted'

            # 작업 수행
            if self.task_complete():
                return 'succeeded'

            rate.sleep()

        return 'failed'

8.2 동시성 컨테이너에서의 선점 관리

동시성 컨테이너에서의 선점은 특별한 고려가 필요하다. 하나의 자식 상태가 종료되었을 때, child_termination_cb을 통해 나머지 자식 상태들의 선점 여부를 결정한다:

def child_termination_cb(outcome_map):
    # 센서 모니터에서 이상 감지 시 모든 병렬 상태 종료
    if outcome_map.get('SENSOR_MONITOR') == 'anomaly':
        return True  # 다른 모든 자식 상태 선점
    return False  # 계속 실행

9. 임무 계획 도구로서의 활용과 한계

9.1 반응적 임무 실행

SMACH는 사전에 정의된 상태 전이 구조를 따르는 반응적 임무 실행(Reactive Mission Execution)에 적합하다. 각 상태에서의 결과에 따라 미리 정의된 전이 경로를 선택하므로, 임무의 실행 흐름이 예측 가능하고 검증 가능하다. 이는 안전성이 중요한 임무(Safety-Critical Mission)에서의 활용에 유리한 특성이다.

그러나 SMACH는 본질적으로 임무 “계획기(Planner)“가 아니라 임무 “실행기(Executor)“에 가깝다는 점을 인식해야 한다. SMACH는 자동적으로 새로운 계획을 생성하거나/최적의 행동 시퀀스를 탐색하지 않으며, 개발자가 사전에 설계한 상태 전이 구조 내에서 임무를 실행한다.

9.2 SMACH의 강점

  • 직관적 모델링: 유한 상태 머신 패러다임은 로봇 임무의 순차적 실행 흐름을 직관적으로 모델링할 수 있다.
  • 계층적 구성: 중첩 컨테이너를 통한 계층적 상태 머신 구성은 복잡한 임무의 모듈화와 재사용성을 향상시킨다.
  • 시각화 지원: smach_viewer를 통한 실시간 상태 시각화는 디버깅과 임무 모니터링을 용이하게 한다.
  • ROS 통합: 액션, 서비스, 토픽과의 원활한 통합은 ROS 기반 로봇 시스템에서의 활용성을 높인다.
  • 선점 메커니즘: 내장된 선점 기능은 동적 임무 전환과 비상 상황 대응을 지원한다.

9.3 SMACH의 한계

  • 상태 폭발(State Explosion) 문제: 임무의 복잡도가 증가할수록 상태 수와 전이 수가 급격히 증가하여 관리가 어려워진다. 상태 수 n에 대해 최악의 경우 전이 수는 O(n^2 \times \lvert\text{outcomes}\rvert)에 달할 수 있다.
  • 정적 구조의 제약: 상태 전이 구조가 설계 시점에 고정되므로, 예상치 못한 상황에 대한 적응적 대응이 제한적이다.
  • 계획 수립 기능의 부재: 목표 상태로부터 필요한 행동 시퀀스를 자동으로 탐색하는 계획 수립 기능이 내재되어 있지 않다.
  • 확장성 제한: 대규모 다중 로봇 시스템에서 각 로봇의 상태 머신을 독립적으로 관리하면서 동시에 조율하는 것이 구조적으로 복잡하다.
  • ROS1 종속성: SMACH는 주로 ROS1을 대상으로 개발되었으며, ROS2로의 완전한 포팅(Porting)은 아직 진행 중이다. ROS2 환경에서는 생명주기 관리 노드와의 통합이 추가적인 개발 노력을 요구한다.

9.4 다른 프레임워크와의 비교

특성SMACH행동 트리(BT)PlanSys2
실행 모델유한 상태 머신트리 순회계획 기반 실행
자동 계획미지원미지원지원 (PDDL)
병렬 실행ConcurrenceParallel Node시간적 계획
런타임 수정제한적유연재계획 가능
시각화smach_viewerGroot터미널 기반
ROS2 지원부분적완전완전
학습 곡선낮음중간높음

10. ROS2 환경에서의 SMACH 활용 전략

10.1 ROS2 포팅 현황

SMACH의 ROS2 호환 버전인 smach_ros2는 커뮤니티 주도로 개발이 진행되고 있다. ROS2 환경에서의 주요 변경 사항은 다음과 같다:

  • rospy 대신 rclpy를 활용한 노드 관리
  • ROS2 액션 인터페이스(rclpy.action)와의 통합
  • LifecycleNode와의 호환을 위한 상태 머신 생명주기 관리
  • 새로운 QoS 프로파일 설정의 지원

10.2 하이브리드 아키텍처

실무적으로, SMACH는 단독으로 사용되기보다는 다른 계획 프레임워크와 결합되는 하이브리드 아키텍처에서 활용되는 경우가 많다. 예를 들어, PlanSys2나 외부 PDDL 플래너가 생성한 계획을 SMACH 상태 머신으로 실행하거나, 행동 트리의 특정 노드에서 SMACH 상태 머신을 호출하는 구조가 가능하다. 이러한 하이브리드 접근법은 SMACH의 직관적 실행 관리 능력과 자동 계획 수립 기능의 장점을 동시에 활용할 수 있다.

11. 참고 문헌

  • Bohren, J., & Cousins, S. (2010). “The SMACH High-Level Executive.” IEEE Robotics & Automation Magazine, 17(4), 18-20.
  • Bohren, J. (2013). “SMACH: Tasks and State Machines.” ROS Wiki Documentation.
  • Colledanchise, M., & Ögren, P. (2018). “Behavior Trees in Robotics and AI: An Introduction.” CRC Press.
  • Quigley, M., Conley, K., Gerkey, B., Faust, J., Foote, T., Leibs, J., … & Ng, A. Y. (2009). “ROS: An Open-Source Robot Operating System.” ICRA Workshop on Open Source Software.
  • Macenski, S., Foote, T., Gerkey, B., Lalancette, C., & Woodall, W. (2022). “Robot Operating System 2: Design, Architecture, and Uses in the Wild.” Science Robotics, 7(66).