21.8.2. 유한 상태 기계(FSM) 아키텍처 코딩
앞서 21.8.1장에서 우리는 페이로드 투하 모듈이 가져야 할 네 가지 필수 상태(IDLE, ARMED, DROPPING, FINISHED)를 머릿속으로 스케치해 보았다.
이제 이 추상적인 개념을 C++ 코드로 단단하게 박제(Hardcoding)할 차례다.
FSM 코드 구조를 짜는 수많은 철학이 있지만, 드론이나 로봇 제어 시스템처럼 ‘단일 스레드(Single Thread) 안에서 루프(Loop)를 도는’ 실시간 시스템에서 가장 교과서적이고 디버깅하기 쉬운 패턴은 단연코 switch-case 기반의 상태 천이 로직이다.
1. FSM의 C++ 클래스 멤버 변수 구성
모듈의 헤더 파일(payload_auto_drop.hpp)에서 이 구조를 담을 그릇을 빚어보자.
class PayloadAutoDrop : public px4::ScheduledWorkItem, public ModuleParams {
public:
// ... 생성자, start(), Run() 선언 ...
private:
// [핵심] 유한 상태 기계의 상태 정의 (Enum Class)
enum class AppState {
STATE_IDLE = 0, // 대기 (안전장치 Locked)
STATE_ARMED, // 무장 (비행 중, 고도 도달 대기)
STATE_DROPPING, // 투하 중 (서보 모터 개방)
STATE_FINISHED // 투하 완료 (영구 잠금)
};
// 현재 상태를 기억하는 '머리(Brain)' 변수.
// 부팅 시점의 최초 상태는 무조건 IDLE 이어야 한다.
AppState _current_state{AppState::STATE_IDLE};
// 상태 변화를 위한 타임스탬프 기록용 변수들
hrt_abstime _drop_start_time{0};
// uORB 토픽 구독/파라미터 변수들 선언... (생략)
};
이렇게 명시적으로 enum class를 선언해 두면, _current_state = 3; 같은 알 수 없는 매직 넘버(Magic Number)를 대입하는 실수를 컴파일러가 원천 차단해 준다. 이것이 C++를 쓰는 C 개발자들의 강력한 무기다.
2. FSM 루프 코어 (Run 함수 내부)
이제 심장부인 Run() 함수 안에서 저 _current_state 변수를 가지고 스위치 패널 조작하듯 코드를 쪼개보자.
void PayloadAutoDrop::Run() {
// 1. 센서 토픽 수집 (이전 장 참고)
update_sensors();
updateParams(); // QGC 파라미터 값(목표 고도 등) 갱신
// 2. 대망의 FSM Main Switch
switch (_current_state) {
case AppState::STATE_IDLE:
run_state_idle();
break;
case AppState::STATE_ARMED:
run_state_armed();
break;
case AppState::STATE_DROPPING:
run_state_dropping();
break;
case AppState::STATE_FINISHED:
run_state_finished();
break;
default:
// 알 수 없는 상태로 빠졌다면 무조건 안전 지대(IDLE)로 회귀!
_current_state = AppState::STATE_IDLE;
break;
}
}
이 구조는 너무나도 직관적이다. 코드를 읽는 누구라도 “지금 밸브가 열려있는(DROPPING) 상태에선 대체 무슨 일이 벌어지고 있지?” 가 궁금하면, 쓸데없는 잡동사니 코드들을 무시하고 오직 run_state_dropping() 함수 하나만 열어보면 된다.
그렇다면 저 잘게 쪼개진 각각의 run_state_xxx() 함수 내부에서는 과연 무엇이 코딩되어야 완벽한 FSM 구조체 패턴이라 불릴 수 있을까?
마구잡이로 모터를 돌리는 것이 아니라 ‘진입(Entry)’, ‘평가(Condition)’, ’퇴장(Exit)’의 3박자가 완벽히 분리된 설계 패턴을 다음 장 21.8.2.1.1(그리고 21.8.2.1장)에서 본격적으로 파헤쳐보자.