21.8.4.2.1. ACK 수신 실패 타임아웃 계산 및 최대 재시도(Max Retries) 도달 시 GCS에 `STATUSTEXT` 에러 로깅을 브로드캐스팅하는 예외 처리 루틴

21.8.4.2.1. ACK 수신 실패 타임아웃 계산 및 최대 재시도(Max Retries) 도달 시 GCS에 STATUSTEXT 에러 로깅을 브로드캐스팅하는 예외 처리 루틴

통신 시스템에서 ’무한 대기(Infinite Wait)’만큼 혐오스러운 버그는 없다.
CONFIRM_ACK 상태에 돌입한 FSM이 하위 데몬의 죽음이나 uORB 버스 트래픽 마비로 인해 영영 영수증(ACK)을 받지 못한다면, 이 모듈은 평생 아무 일도 하지 못하는 좀비가 되어버린다.

이러한 재난을 방어하기 위해 FSM 루프 안에 시한폭탄(Timeout Timer)과 제한된 재시도 횟수(Max Retries)라는 두 가지 방어선을 쳐야 한다.

1. 타임아웃 타이머와 재시도 로직의 이식

PayloadAutoDrop 클래스 헤더에 두 개의 멤버 변수를 추가한다.

// 헤더 파일 추가 변수
hrt_abstime _ack_wait_start_time{0}; // ACK 대기를 시작한 시점
uint8_t     _command_retry_count{0}; // 현재까지 재시도한 횟수
const uint8_t MAX_RETRIES = 3;       // 최대 3번까지만 다시 물어본다.

이제 앞장의 CONFIRM_ACK 상태 루프를 타임아웃 방어선이 포함된 무결점 코드로 업그레이드해보자.

void PayloadAutoDrop::run_state_confirm_ack() {
    
    // [Entry Action] 방금 이 상태로 넘어왔을 때 타이머를 리셋한다.
    if (_state_just_entered) {
        _ack_wait_start_time = hrt_absolute_time();
        _state_just_entered = false;
    }

    // 1. [Do Action] 통상적인 ACK 수신 확인 파트
    vehicle_command_ack_s ack{};
    if (_command_ack_sub.update(&ack)) {
        if (ack.command == vehicle_command_s::VEHICLE_CMD_DO_SET_ACTUATOR) {
            if (ack.result == vehicle_command_ack_s::VEHICLE_CMD_RESULT_ACCEPTED) {
                // 대성공! 
                _current_state = AppState::RTL;
                return; // 루프를 즉시 탈출
            }
        }
    }

    // 2. [Fail-Safe] 타임아웃(Timeout) 계산 파트
    // ACK가 안 온 상태에서 얼만큼의 시간이 흘렀는가?
    hrt_abstime now = hrt_absolute_time();
    float wait_time_s = (now - _ack_wait_start_time) / 1e6f;

    // 만약 0.5초가 넘도록 대답이 없다면? (Timeout 발생)
    if (wait_time_s > 0.5f) {
        
        _command_retry_count++; // 재시도 카운트 1 증가
        
        if (_command_retry_count <= MAX_RETRIES) {
            // [플랜 B] 아직 기회가 남았다. 
            // 상태를 뒤로 되돌려 명령 발송(TRIGGER)부터 다시 시작한다!
            PX4_WARN("ACK Timeout. Retrying Command... (%d/%d)", 
                     _command_retry_count, MAX_RETRIES);
            
            _current_state = AppState::TRIGGER;
            _state_just_entered = true; // 진입 액션을 켜주어야 모터 명령이 다시 나간다.
            
        } else {
            // [플랜 C: 최악의 상황] 3번이나 다시 보냈는데도 대답이 없다!
            // 하드웨어 핀이 타버렸거나 통신선이 절단된 치명적 상황이다.
            
            // 1. 조종사의 화면(QGC) 한가운데에 새빨간 에러 메시지를 띄워 알린다!
            mavlink_log_critical(&_mavlink_log_pub, 
                                 "CRITICAL: Payload actuator failed to respond!");
            
            // 2. 미련 없이 미션을 포기하고 남은 시스템이라도 살리기 위해 상태를 넘긴다.
            _command_retry_count = 0; // 카운트 초기화
            _current_state = AppState::RTL; 
        }
    }
}

2. STATUSTEXT 브로드캐스팅의 위력

코드 하단부의 가장 치명적인 순간에 등장한 mavlink_log_critical() 매크로는 단순한 디버깅 용도의 PX4_ERR() 따위와는 궤를 달리한다.

PX4_ERR()는 USB 케이블을 꼽고 콘솔창(nsh)을 들여다보는 개발자의 눈에만 보이는 로그다. 반면 mavlink_log_critical(&_mavlink_log_pub, ...) 함수를 때리면, 커널 공간에서 uORB의 mavlink_log_s 토픽이 전 영역으로 뿜어져 나간다.
이 텔레메트리 신호는 무선 주파수(RF) 안테나를 타고 날아가, 수 킬로미터 밖에서 QGroundControl을 보고 있는 조종사의 메인 화면 중앙에 “뚜-둑!” 하는 소리와 함께 큼지막한 빨간색 에러 배너(STATUSTEXT Message)를 띄워준다.

조종사는 이 팝업을 보고 밸브가 열리지 않았음을 즉각 소스 코드가 아닌 ’화면’으로 체감하고, 지체 없이 드론을 기지로 복귀시키는 비상 수동 조작을 결단할 수 있게 되는 것이다.


이로써 FSM 설계도, 노이즈 필터링 보수도, 그리고 최악의 상황을 대비한 예외 처리 비상 시스템까지 완벽하게 코딩이 끝났다. ’페이로드 구동 모듈’의 심장이 독립적으로 고동치기 시작했다.

남은 것은 이 위대한 모듈을 픽스호크 깡통 안에 욱여넣고, 전원을 켤 때마다 자동으로 숨을 쉬게(데몬화, Daemonization) 만드는 커널 부팅 관문 통과뿐이다. 대망의 21.9장, 펌웨어 부트 시퀀스 연동의 세계로 진입하자.