1296.14 비동기 액션 노드의 onStart() 콜백

1. onStart() 콜백의 정의

onStart()StatefulActionNode의 세 가지 콜백 중 첫 번째로 호출되는 순수 가상 메서드이다. 노드의 상태가 IDLE일 때 executeTick()이 호출되면, 내부적으로 onStart()를 호출하여 행동의 초기화와 시작을 수행한다. 이 콜백은 비동기 행동의 진입점(entry point)이며, 행동의 생명주기에서 한 번만 호출된다.

virtual NodeStatus onStart() = 0;

onStart()NodeStatus::SUCCESS, NodeStatus::FAILURE, NodeStatus::RUNNING 중 하나를 반환하여야 하며, NodeStatus::IDLE을 반환하면 LogicError 예외가 발생한다.

2. onStart()의 호출 조건

onStart()가 호출되는 조건은 executeTick() 시점에서 노드의 상태가 IDLE인 경우이다. 이 조건이 충족되는 시나리오는 다음과 같다.

  1. 최초 Tick: 노드가 생성된 후 한 번도 Tick되지 않은 경우, 초기 상태인 IDLE에서 onStart()가 호출된다.
  2. 완료 후 재실행: 이전 실행이 SUCCESS 또는 FAILURE로 완료된 후, 부모 노드에 의해 상태가 IDLE로 초기화되고 다시 Tick되는 경우.
  3. Halt 후 재실행: RUNNING 상태에서 halt()가 호출되어 IDLE로 전이한 후, 다시 Tick되는 경우.

세 가지 시나리오 모두에서 onStart()는 행동을 처음부터 시작하는 역할을 수행한다. 이전 실행의 잔여 상태에 의존하지 않고, 입력 포트의 현재 값에 기반하여 행동을 초기화하여야 한다.

3. onStart()의 역할

3.1 입력 포트 데이터 읽기

onStart()의 첫 번째 역할은 입력 포트에서 행동에 필요한 데이터를 읽는 것이다. 입력 포트의 값은 onStart() 호출 시점에서 읽는 것이 권장된다. 이는 행동의 시작 시점에서의 블랙보드 상태를 기준으로 행동이 수행됨을 보장한다.

BT::NodeStatus NavigateToPose::onStart()
{
    geometry_msgs::msg::PoseStamped goal;
    if (!getInput("goal", goal))
    {
        RCLCPP_ERROR(logger_, "목표 포즈 읽기 실패");
        return BT::NodeStatus::FAILURE;
    }

    double speed;
    getInput("max_speed", speed);  // 기본값 사용 가능

    // goal과 speed를 사용하여 행동 시작
    // ...
}

입력 포트의 필수 값이 누락되면 onStart()에서 즉시 FAILURE를 반환하여야 한다. 이 경우 onRunning()은 호출되지 않으며, 행동은 단일 Tick 내에서 실패로 종료된다.

3.2 내부 상태 초기화

onStart()의 두 번째 역할은 멤버 변수를 초기화하여 다중 Tick 실행을 위한 상태를 준비하는 것이다.

BT::NodeStatus MoveArm::onStart()
{
    geometry_msgs::msg::Pose target;
    if (!getInput("target_pose", target))
        return BT::NodeStatus::FAILURE;

    // 멤버 변수 초기화
    goal_accepted_ = false;
    result_received_ = false;
    start_time_ = std::chrono::steady_clock::now();

    // 목표 전송
    auto goal_msg = moveit_msgs::action::MoveGroup::Goal();
    goal_msg.request.goal_constraints = createConstraints(target);
    goal_handle_future_ = action_client_->async_send_goal(goal_msg);

    return BT::NodeStatus::RUNNING;
}

내부 상태의 초기화는 이전 실행의 잔여 상태가 현재 실행에 영향을 미치는 것을 방지한다. Halt 후 재실행 시 이전 실행의 goal_handle_이나 result_future_가 유효하지 않은 상태일 수 있으므로, onStart()에서 모든 관련 멤버 변수를 명시적으로 초기화하는 것이 필수적이다.

3.3 비동기 작업 시작

onStart()의 세 번째 역할은 외부 시스템에 대한 비동기 작업을 시작하는 것이다. 이는 ROS2 액션 목표 전송, 서비스 요청, 타이머 시작 등의 형태로 나타난다.

BT::NodeStatus CallService::onStart()
{
    auto request = std::make_shared<ServiceType::Request>();
    if (!populateRequest(request))
        return BT::NodeStatus::FAILURE;

    // 비동기 서비스 호출
    result_future_ = service_client_->async_send_request(request);

    return BT::NodeStatus::RUNNING;
}

비동기 작업의 시작은 비차단적으로 수행되어야 한다. async_send_goal(), async_send_request() 등의 비동기 API를 사용하여 요청을 전송하고 즉시 반환한다.

4. onStart()의 반환값 분석

onStart()의 세 가지 유효한 반환값의 의미와 사용 조건을 분석한다.

4.1 RUNNING 반환

가장 일반적인 반환값이다. 행동이 성공적으로 시작되었으며, 완료까지 추가 Tick이 필요함을 나타낸다.

BT::NodeStatus onStart() override
{
    // 입력 검증 통과
    // 내부 상태 초기화 완료
    // 비동기 작업 시작됨
    return BT::NodeStatus::RUNNING;
}

RUNNING이 반환되면, 다음 Tick부터 onRunning()이 호출된다.

4.2 FAILURE 반환

행동을 시작할 수 없는 경우에 반환한다. 입력 포트의 값이 유효하지 않거나, 외부 시스템에 연결할 수 없거나, 전제 조건이 충족되지 않은 경우가 해당된다.

BT::NodeStatus onStart() override
{
    geometry_msgs::msg::PoseStamped goal;
    if (!getInput("goal", goal))
        return BT::NodeStatus::FAILURE;

    if (!action_client_->wait_for_action_server(
            std::chrono::milliseconds(10)))
    {
        RCLCPP_WARN(logger_, "액션 서버 미가용");
        return BT::NodeStatus::FAILURE;
    }

    // 행동 시작 ...
    return BT::NodeStatus::RUNNING;
}

FAILURE가 반환되면, 노드는 RUNNING 상태를 거치지 않고 즉시 FAILURE 상태로 전이한다. onRunning()onHalted()는 호출되지 않는다.

4.3 SUCCESS 반환

행동이 onStart() 호출 시점에서 즉시 완료되는 경우에 반환한다. 이 경우 StatefulActionNodeSyncActionNode와 동일하게 동작한다.

BT::NodeStatus onStart() override
{
    std::string cached_result;
    if (getInput("cached_result", cached_result) 
        && !cached_result.empty())
    {
        // 캐시된 결과가 있으면 즉시 완료
        setOutput("result", cached_result);
        return BT::NodeStatus::SUCCESS;
    }

    // 캐시 미중(miss): 비동기 작업 시작
    result_future_ = computeAsync();
    return BT::NodeStatus::RUNNING;
}

이 패턴은 캐시 적중(cache hit) 시 불필요한 다중 Tick 실행을 회피하는 최적화에 유용하다.

5. ROS2 액션 클라이언트의 onStart() 구현 패턴

ROS2 액션 클라이언트를 래핑하는 StatefulActionNode에서 onStart()의 표준 구현 패턴을 제시한다.

BT::NodeStatus onStart() override
{
    // 1. 입력 포트 읽기
    geometry_msgs::msg::PoseStamped goal_pose;
    if (!getInput("goal", goal_pose))
        return BT::NodeStatus::FAILURE;

    // 2. 액션 서버 가용성 확인 (짧은 타임아웃)
    if (!action_client_->wait_for_action_server(
            std::chrono::milliseconds(10)))
    {
        RCLCPP_WARN(logger_, "액션 서버가 가용하지 않음");
        return BT::NodeStatus::FAILURE;
    }

    // 3. 목표 메시지 구성
    auto goal_msg = ActionType::Goal();
    goal_msg.pose = goal_pose;

    // 4. 피드백 콜백 설정 (선택적)
    auto send_goal_options = 
        rclcpp_action::Client<ActionType>::SendGoalOptions();
    send_goal_options.feedback_callback =
        [this](auto,%20auto%20feedback)
        {
            last_feedback_ = feedback;
        };

    // 5. 비동기 목표 전송
    goal_handle_future_ = action_client_->async_send_goal(
        goal_msg, send_goal_options);

    // 6. 내부 상태 초기화
    goal_accepted_ = false;

    return BT::NodeStatus::RUNNING;
}

이 패턴의 각 단계는 다음의 설계 원칙을 따른다.

  1. 입력 검증 우선: 외부 시스템과의 상호 작용 전에 입력의 유효성을 검증한다.
  2. 가용성 확인: 액션 서버의 가용성을 짧은 타임아웃으로 확인하여, 서버 부재 시 불필요한 대기를 방지한다.
  3. 비동기 전송: async_send_goal()로 비차단적으로 목표를 전송한다.
  4. 상태 초기화: onRunning()에서 참조할 멤버 변수를 초기화한다.

6. 타이머 기반 행동의 onStart() 구현

시간 기반 행동에서 onStart()는 시작 시간 또는 마감 시간을 기록한다.

BT::NodeStatus WaitForDuration::onStart()
{
    double duration;
    if (!getInput("duration", duration) || duration < 0.0)
        return BT::NodeStatus::FAILURE;

    if (duration == 0.0)
        return BT::NodeStatus::SUCCESS;  // 즉시 완료

    deadline_ = std::chrono::steady_clock::now()
              + std::chrono::duration<double>(duration);

    return BT::NodeStatus::RUNNING;
}

대기 시간이 0인 경우 즉시 SUCCESS를 반환하는 것은 경계 조건(boundary condition)의 올바른 처리이다.

7. onStart()에서의 예외 처리

onStart() 내부에서 발생하는 예외는 메서드 내부에서 포착하여 FAILURE로 변환하여야 한다.

BT::NodeStatus TransformGoal::onStart()
{
    geometry_msgs::msg::PoseStamped goal;
    if (!getInput("goal", goal))
        return BT::NodeStatus::FAILURE;

    try
    {
        auto transformed = tf_buffer_->transform(
            goal, "map", tf2::durationFromSec(0.1));
        transformed_goal_ = transformed;
    }
    catch (const tf2::TransformException& ex)
    {
        RCLCPP_ERROR(logger_, "좌표 변환 실패: %s", ex.what());
        return BT::NodeStatus::FAILURE;
    }

    goal_handle_future_ = action_client_->async_send_goal(
        createGoalMsg(transformed_goal_));

    return BT::NodeStatus::RUNNING;
}

예외가 onStart() 외부로 전파되면 executeTick()에서 처리되지 않아 트리 전체의 실행이 중단될 수 있다. 따라서 외부 라이브러리 호출, 타입 변환, 자원 할당 등의 예외 발생 가능한 연산은 반드시 try-catch 블록으로 감싸야 한다.

8. onStart()의 설계 지침

onStart() 콜백의 구현 시 준수하여야 하는 설계 지침을 정리한다.

  1. 비차단 실행: onStart() 내부에서 차단 호출을 수행하지 않는다. ROS2 액션 서버의 가용성 확인 시에도 짧은 타임아웃(수십 ms 이내)을 사용한다.

  2. 완전한 초기화: 이전 실행의 잔여 상태가 현재 실행에 영향을 미치지 않도록, 모든 관련 멤버 변수를 onStart()에서 명시적으로 초기화한다.

  3. 입력 포트의 일회 읽기: 입력 포트의 값은 onStart()에서 한 번 읽고 멤버 변수에 저장한다. onRunning()에서 입력 포트를 반복적으로 읽으면, Tick 간에 블랙보드 값이 변경되어 행동의 일관성이 저해될 수 있다.

  4. 조기 반환 패턴: 행동을 시작할 수 없는 조건을 먼저 검사하고, 조건이 충족되지 않으면 즉시 FAILURE를 반환한다. 비동기 작업의 시작은 모든 전제 조건이 충족된 후에 수행한다.

  5. IDLE 반환 금지: onStart()에서 NodeStatus::IDLE을 반환하면 LogicError 예외가 발생한다. IDLE은 노드의 초기 상태이지 실행 결과가 아니다.