목표·피드백·결과
85분 안팎
학습 목표
이동 작업의 대기·실행·성공·실패·취소 전이를 구현합니다.
개념
접수와 완료 사이의 시간을 모델링합니다
관제가 이동 요청을 보냈다고 로봇이 도착한 것은 아닙니다. 이동 역할은 요청을 받아 관리하기 시작하고, 다음 실행 기회에 제어 작업을 시작하며, 중간 거리와 최종 결과를 남겨야 합니다. 이번 레슨에서는 이 수명을 상태 기계로 구현합니다. 브라우저 과제는 하나의 이미 접수된 목표를 대상으로 이벤트 열을 처리하며 잘못된 전이가 결과를 바꾸지 못하게 합니다.
PC 모델은 ACCEPTED를 접수 대기, EXECUTING을 실행, CANCELING을 취소 처리, SUCCEEDED·ABORTED·CANCELED를 종료로 사용합니다. 일상적인 실패라는 말은 ABORTED에 대응합니다. 입력 거절은 접수 이전 판단이므로 이 목표 상태 열에 REJECTED를 넣지 않습니다. 접수도 안 된 요청에 피드백을 보내는 모순을 방지하기 위한 구분입니다.
목표·피드백·결과를 세 기록으로 나눕니다
모듈 미션의 목표는 id, x_m, y_m, frame_id, max_speed_mps입니다. 좌표는 map 기준 m이며 속도 상한은 m/s입니다. 같은 숫자라도 odom이나 sensor 기준이면 다른 목표가 될 수 있어 frame_id를 생략하지 않습니다. 이 단계의 미션은 map만 허용하고 다른 프레임은 거절합니다. 좌표 변환을 조용히 추측해 적용하지 않습니다.
피드백에는 id, distance_m, tick이 들어갑니다. 거리 계산은 외부에서 주입한 map 위치와 목표 사이의 hypot입니다. 결과에는 id, status, reason, tick을 남깁니다. 피드백은 계속 바뀔 수 있지만 종료 결과는 한 번 확정되면 보존합니다. 결과가 없는 실행 중 상태와 거리 값이 0인 피드백을 같은 것으로 보지 않습니다.
전이표를 조건과 함께 읽습니다
ACCEPTED에서 START를 받으면 EXECUTING입니다. EXECUTING에서 SUCCEED는 SUCCEEDED, FAIL은 ABORTED입니다. ACCEPTED와 EXECUTING에서 CANCEL은 CANCELING이며, CANCELING에서 STOPPED를 받으면 CANCELED입니다. 피드백은 EXECUTING에서만 처리하고 상태를 바꾸지 않습니다. 같은 START를 두 번 받는 것도 허용된 전이가 아닙니다.
CANCELING에서 SUCCEED가 들어오면 도착 판단으로 취소를 덮어쓰지 않습니다. 이 과제는 취소 접수를 먼저 관찰한 경우 취소 종료를 기다리는 정책을 선택합니다. 같은 순간에 도착과 취소가 생길 수 있으므로 정책을 전이표와 테스트에 명시합니다. 모든 제품이 같은 우선순위를 써야 한다는 뜻은 아니며 구현자가 계약을 일관되게 적용해야 합니다.
불가능한 전이는 원래 상태를 보존합니다
브라우저 출력은 정상 전이면 현재 상태 한 행, 잘못된 이벤트면 INVALID와 원래 상태 한 행입니다. ACCEPTED에서 SUCCEED를 받으면 INVALID ACCEPTED이며 상태는 유지됩니다. 다음 START는 정상 실행으로 갈 수 있습니다. 잘못된 이벤트가 왔다고 목표를 삭제하면 호출자는 무엇이 종료됐는지 알 수 없고 이후 유효한 이벤트도 처리할 수 없습니다.
종료 상태에는 허용 전이를 두지 않습니다. SUCCEEDED에서 CANCEL을 받으면 INVALID SUCCEEDED입니다. 늦게 도착한 메시지가 이전 결과를 취소 결과로 덮어쓰지 못하게 하는 규칙입니다. 실제 서버의 취소 응답 코드와 이 교육용 INVALID 문자열은 다른 인터페이스입니다. 의미를 비교하되 이름이 같다고 가정하지 않습니다.
피드백은 수치 계약도 검사합니다
FEEDBACK 1.25는 실행 중일 때만 유효하며 거리 값은 유한한 0 이상 숫자입니다. 유효하면 EXECUTING distance=1.250처럼 소수점 세 자리로 출력합니다. FEEDBACK -1, FEEDBACK NaN, FEEDBACK inf는 INVALID EXECUTING입니다. Python float 변환이 됐다고 물리적으로 쓸 수 있는 값이라고 판단하지 않습니다.
피드백 거리가 0이어도 이 이벤트 자체로 SUCCEEDED가 되지는 않습니다. 성공 판정 이벤트가 별도로 있어야 합니다. 이 과제는 전이 책임을 분리하기 위한 모델이고 미션에서는 tick이 거리 임계값 0.05m와 HAL 정지를 함께 확인합니다. 브라우저의 SUCCEED는 이미 그 검사를 통과했다는 시험 입력 역할입니다.
목표 식별자는 전이 밖의 책임도 담습니다
목표 id는 서로 다른 작업 기록을 섞지 않게 합니다. 미션은 같은 id와 동일한 목표가 다시 오면 DUPLICATE로 답하고 새 작업을 만들지 않습니다. 같은 id로 좌표가 달라지면 CONFLICT입니다. 동시에 활성 목표는 하나로 제한하며 새 id는 BUSY로 거절합니다. 이 정책은 실습 범위를 명확히 하려는 선택으로 다중 목표 액션 서버의 일반 규칙은 아닙니다.
중복 검사는 활성 여부 검사보다 먼저 합니다. 그렇지 않으면 실행 중 같은 요청을 다시 보냈을 때 기록을 찾지 못하고 BUSY만 보게 됩니다. 종료된 목표의 같은 요청도 DUPLICATE이고 원래 결과를 보존합니다. 딕셔너리 기록은 프로세스 메모리여서 재시작 후 같은 보장은 없습니다. 영속 중복 방지가 필요하면 별도 저장과 수명 정책을 설계합니다.
이벤트 열에서 가장 먼저 어긋난 지점을 찾습니다
정상 열 START, FEEDBACK 1, SUCCEED를 넣어 접수에서 실행과 성공으로 가는지 확인합니다. 그 다음 취소 전 시작하지 않는 경로 CANCEL, STOPPED를 시험합니다. 마지막으로 START, CANCEL, SUCCEED, STOPPED를 넣어 취소 처리 중 성공 이벤트가 무시되고 취소 종료가 남는지 봅니다. 결과 한 줄만 맞추지 말고 매 이벤트 직후 상태를 비교합니다.
starter는 모든 이벤트를 INVALID로 반환합니다. 전이표 조회와 피드백 분기를 분리해 구현하면 누락을 찾기 쉽습니다. KeyError는 표에 없는 상태나 이벤트를 직접 인덱싱했을 가능성을 뜻합니다. dict.get을 이용해 허용되지 않은 조합을 판정하고, 예외를 삼켜 빈 출력으로 끝내지 않습니다. 출력 행 수는 입력 이벤트 행 수와 같아야 합니다.
모사 상태와 실제 액션 통신의 차이를 남깁니다
브라우저에는 실제 목표 전송, 피드백 토픽 구독, 결과 future와 실행기 콜백이 없습니다. 이벤트 문자열은 테스트 장치가 넣는 입력입니다. ROS 2 액션 설계의 상태 구분을 참고하되 이 코드가 ActionServer를 띄웠다고 설명하지 않습니다. 실제 타입 생성과 목표 핸들 처리 예제는 더 읽기에서 확인합니다.
실제 환경에서는 목표 수락 응답을 받은 뒤 결과 요청을 관리하고, 피드백의 목표 식별자를 확인하며, 취소 접수와 취소 종료를 나누어 관찰합니다. 특정 클라이언트 화면의 버튼이 사라졌다는 사실은 서버 작업 종료 증거가 아닙니다. 작업 결과를 조회하는 주체가 중간에 재연결하는 경우도 고려할 수 있게 종료 기록의 책임을 분명히 합니다.
전이 구현을 다음 단계에 인계합니다
이번 레슨의 핵심 제출물은 코드와 전이표입니다. 허용 조합뿐 아니라 ACCEPTED→SUCCEEDED 같은 금지 조합도 적고 기대하는 오류 문자열을 붙입니다. 입력에 공백만 있는 행도 빈 이벤트이므로 INVALID를 출력하며, 빈 입력 파일은 어떤 목표 처리 이벤트도 없어 출력하지 않습니다. 이 차이를 경계 사례로 남기면 입력 처리 오류를 쉽게 찾습니다.
다음 레슨은 STOPPED에 해당하는 증거를 C HAL에서 얻습니다. 상태 이름을 바꾸는 것만으로 정지 요청이 전송되지는 않습니다. 전이표가 올바르더라도 마지막 PWM을 유지하면 모터 출력 계약은 실패합니다. 논리 상태와 구동 출력의 연결은 별도 회귀 검사로 확인해야 하며 이번 상태 기계가 그 연결 지점을 제공합니다.
사실 확인 참고: ROS 2 공식 액션 설계의 목표 수명과 상태 구분을 확인합니다.
따라하기
허용 전이를 먼저 표현합니다
두 이벤트를 전이표로 처리합니다.
state="ACCEPTED"
table={("ACCEPTED","START"):"EXECUTING",("EXECUTING","SUCCEED"):"SUCCEEDED"}
for event in ("START","SUCCEED"):
state=table[(state,event)]
print(state)실행 결과
EXECUTING SUCCEEDED
종료 뒤 이벤트를 차단합니다
종료 상태에 허용 전이가 없음을 확인합니다.
state="SUCCEEDED"
table={("EXECUTING","CANCEL"):"CANCELING"}
print("INVALID "+state if (state,"CANCEL") not in table else table[(state,"CANCEL")])실행 결과
INVALID SUCCEEDED
거리 경계값을 검사합니다
NaN도 float 변환은 가능하므로 유한성과 음수 여부를 따로 검사합니다.
import math
for raw in ("0","1.25","-1","NaN","inf"):
value=float(raw)
print(raw,"VALID" if math.isfinite(value) and value>=0 else "INVALID")실행 결과
0 VALID 1.25 VALID -1 INVALID NaN INVALID inf INVALID
취소 우선 정책을 재현합니다
브라우저에 START, CANCEL, SUCCEED, STOPPED를 각각 한 행으로 넣습니다. SUCCEED가 INVALID CANCELING이고 마지막이 CANCELED인지 확인합니다. 성공 뒤 취소와 START 반복도 추가로 시험합니다.
확인 문제
실습
이미 접수된 목표의 초기 상태는 ACCEPTED입니다. 표준 입력 한 행에 이벤트 하나를 받습니다. ACCEPTED+START→EXECUTING, EXECUTING+SUCCEED→SUCCEEDED, EXECUTING+FAIL→ABORTED, ACCEPTED/EXECUTING+CANCEL→CANCELING, CANCELING+STOPPED→CANCELED입니다. 정상 전이는 새 상태 한 행을 출력합니다. EXECUTING에서 FEEDBACK 숫자는 유한한 0 이상이면 상태를 유지하고 EXECUTING distance=값(소수점 세 자리)을 출력합니다. 그 외 입력·전이는 INVALID 현재상태를 출력하고 상태를 유지합니다. 종료 뒤 전이는 모두 금지합니다. 이벤트 이름은 대문자이며 단어 수가 달라도 INVALID입니다. 빈 파일은 출력하지 않으며 빈 행은 INVALID입니다.
모범 답안
import sys, math
state="ACCEPTED"
transitions={
("ACCEPTED","START"):"EXECUTING", ("ACCEPTED","CANCEL"):"CANCELING",
("EXECUTING","SUCCEED"):"SUCCEEDED", ("EXECUTING","FAIL"):"ABORTED",
("EXECUTING","CANCEL"):"CANCELING", ("CANCELING","STOPPED"):"CANCELED"
}
for line in sys.stdin:
parts=line.split()
if len(parts)==2 and parts[0]=="FEEDBACK" and state=="EXECUTING":
try: distance=float(parts[1])
except ValueError: distance=float("nan")
if math.isfinite(distance) and distance>=0:
print(f"EXECUTING distance={distance:.3f}");continue
target=transitions.get((state,parts[0])) if len(parts)==1 else None
if target is None: print("INVALID "+state)
else: state=target;print(state)
더 읽기
면접 질문
- ROS 2 토픽과 서비스, 액션의 선택 기준을 설명해 주시면 됩니다.