제6부 하드웨어 인터페이스
6.9 진단 — diagnostic_updater와 상태 보고
6.9.1 이 장에서 배우는 것 / 선수 지식
이 책은 여러 장에서 같은 말을 반복했다 — "카운터를 진단으로 내보내라." 실시간 경로의 로깅 대신(1.11.7), tf 조회 실패율을(5.2.8), 하드웨어 통신 오류율을(6.2.10), 드라이버 상태를(6.7.9), 제어 주기 초과를(6.6.8). 이 장이 그 "진단"의 실체다.
진단이 중요한 이유는 명확하다. 로봇은 조용히 악화된다. 케이블이 마모되고, 센서가 더러워지고, 통신 오류율이 서서히 오른다. 완전히 멈추기 전까지는 아무도 모르고, 멈춘 뒤에는 원인을 찾기 어렵다. 진단은 그 사이를 메운다.
배우는 것:
- 진단 메시지 구조 —
DiagnosticArray와 상태 레벨. diagnostic_updater— 태스크 기반 구현.- 표준 태스크 — 주파수·타임스탬프 감시.
- 집계기 — 많은 항목을 계층으로 정리.
- 설계 원칙 — 무엇을 어느 레벨로 보고할 것인가.
- 실시간 경로와의 연결 — 카운터 패턴 완성.
- 운영 활용 — 시각화·경보·기록.
- 워치독으로서의 진단(6.9.14) — systemd가 못 보는 실패를 검출하는 법과 그 조건.
선수 지식: 1.11(로깅·카운터 패턴), 6.2.10·6.6.8·6.7.9(진단 항목들), 1.4(토픽), 2.9(원자 연산).
정직:
diagnostic_updaterAPI는 ROS 1에서 이어져 안정적인 편이다.Updater의 갱신 주기 결정 규칙과 진단 이름 형식은 Jazzy에서 직접 컴파일·실행해 확인했다(6.9.12). 집계기(diagnostic_aggregator) 설정 형식은 확인하지 않았으므로 패키지 문서를 정본으로 보라(목차 3).
6.9.2 진단 메시지 구조
레벨 네 가지:
| 레벨 | 값 | 의미 | 운영 대응 |
|---|---|---|---|
OK | 0 | 정상 | 없음 |
WARN | 1 | 비정상이지만 동작 중 | 주의·추적 |
ERROR | 2 | 기능 실패 | 조치 필요 |
STALE | 3 | 보고가 끊김 | 노드 확인 |
STALE이 특별하다. 이것은 노드가 보고한 것이 아니라 집계기가 판단하는 것이다 — 일정 시간 갱신이 없으면 STALE로 표시된다. 즉 "진단을 보내던 노드가 종료됐다"를 감지한다.
견해:
STALE이 진단 체계의 숨은 가치다. 노드가 크래시하면 그 노드는 아무것도 보고하지 못하는데, 집계기가 그 침묵을 감지해 알려 준다.AUTOMATICliveliness로는 검출되지 않는 상태(1.5.3.6)도 여기서 드러난다.
values가 실제 데이터다:
name: "모터 드라이버: 통신"
level: WARN
message: "오류율 상승"
hardware_id: "arm_ctrl_01"
values:
- {key: "오류율(%)", value: "2.4"}
- {key: "최근 1분 오류 수", value: "144"}
- {key: "평균 왕복(ms)", value: "1.8"}
- {key: "최대 왕복(ms)", value: "12.3"}
- {key: "재연결 횟수", value: "3"}
견해:
message에는 사람이 읽을 요약을,values에는 수치를 넣으라. 운영자는message를 보고 판단하고, 엔지니어는values를 보고 진단한다. 둘을 섞으면 양쪽 모두에게 불편해진다.
6.9.3 diagnostic_updater 기본
#include "diagnostic_updater/diagnostic_updater.hpp"
#include "diagnostic_updater/publisher.hpp"
class MyDriverNode : public rclcpp::Node
{
public:
MyDriverNode() : Node("my_driver"), updater_(this)
{
updater_.setHardwareID("arm_ctrl_01");
// 태스크 등록 — 주기적으로 호출된다
updater_.add("통신", this, &MyDriverNode::diagnose_communication);
updater_.add("모터 상태", this, &MyDriverNode::diagnose_motors);
updater_.add("온도", this, &MyDriverNode::diagnose_temperature);
// 갱신 주기 (기본 1 Hz — 파라미터 diagnostic_updater.period 로도 설정 가능)
updater_.setPeriod(1.0);
}
private:
void diagnose_communication(diagnostic_updater::DiagnosticStatusWrapper & stat)
{
// 실시간 경로가 갱신한 원자 카운터를 읽는다 (6.9.6)
const auto errors = comm_errors_.exchange(0);
const auto total = comm_total_.exchange(0);
const double rate = total > 0 ? 100.0 * errors / total : 0.0;
if (total == 0) {
stat.summary(diagnostic_msgs::msg::DiagnosticStatus::ERROR, "통신 없음");
} else if (rate > 5.0) {
stat.summary(diagnostic_msgs::msg::DiagnosticStatus::ERROR,
"통신 오류율 심각");
} else if (rate > 0.5) {
stat.summary(diagnostic_msgs::msg::DiagnosticStatus::WARN,
"통신 오류율 상승");
} else {
stat.summary(diagnostic_msgs::msg::DiagnosticStatus::OK, "정상");
}
stat.add("오류율(%)", rate);
stat.add("최근 오류 수", errors);
stat.add("최근 시도 수", total);
stat.add("평균 왕복(ms)", rtt_avg_ms_.load());
stat.add("최대 왕복(ms)", rtt_max_ms_.exchange(0.0));
stat.add("재연결 횟수", reconnects_.load());
}
diagnostic_updater::Updater updater_;
std::atomic<uint64_t> comm_errors_{0}, comm_total_{0}, reconnects_{0};
std::atomic<double> rtt_avg_ms_{0.0}, rtt_max_ms_{0.0};
};
동작: Updater가 타이머로 등록된 태스크들을 호출하고, 결과를 모아 /diagnostics에 발행한다.
| 요소 | 역할 |
|---|---|
setHardwareID | 물리 장치 식별(여러 대 운영 시 중요) |
add(이름, 콜백) | 태스크 등록 |
stat.summary(레벨, 메시지) | 레벨·요약 설정 |
stat.add(키, 값) | 수치 추가 |
setPeriod | 갱신 주기 |
함정:
Updater를 멤버로 보관하지 않으면 소멸해 동작하지 않는다. 5.1.3(tf 리스너)·2.8.8(rclpy 구독)과 같은 성질의 실수다.
함정: 태스크 콜백은 executor 스레드에서 실행된다. 여기서 블로킹하면 다른 콜백이 지연된다(2.2.9). 하드웨어를 직접 읽지 말고, 이미 수집된 값을 보고하는 데 그쳐야 한다.
6.9.4 표준 태스크
직접 만들지 않아도 되는 흔한 감시들이다.
6.9.4.1 주파수 감시
#include "diagnostic_updater/update_functions.hpp"
double min_freq = 9.0, max_freq = 11.0;
freq_status_ = std::make_unique<diagnostic_updater::FrequencyStatus>(
diagnostic_updater::FrequencyStatusParam(&min_freq, &max_freq, 0.1, 10));
updater_.add(*freq_status_);
// 발행할 때마다 호출
void publish_scan(...) {
pub_->publish(msg);
freq_status_->tick();
}
센서가 기대 주기로 오는지 감시한다. 카메라가 30 Hz여야 하는데 12 Hz면 WARN·ERROR가 출력된다 — USB 대역 부족·CPU 과부하·센서 고장의 조기 신호다.
6.9.4.2 타임스탬프 감시
double min_delay = -1.0, max_delay = 0.5; // 초
stamp_status_ = std::make_unique<diagnostic_updater::TimeStampStatus>(
diagnostic_updater::TimeStampStatusParam(min_delay, max_delay));
updater_.add(*stamp_status_);
void on_message(const Msg::SharedPtr msg) {
stamp_status_->tick(msg->header.stamp);
}
데이터의 나이를 감시한다. 1.10.7의 "데이터 나이 검사"와 5.2.4의 "시계 불일치 탐지"가 여기서 자동화된다 — 스탬프가 미래면 시계 문제, 너무 과거면 지연 문제다.
6.9.4.3 발행자 결합
#include "diagnostic_updater/publisher.hpp"
diagnostic_updater::DiagnosedPublisher<sensor_msgs::msg::LaserScan> diag_pub_(
node->create_publisher<sensor_msgs::msg::LaserScan>("scan", 10),
updater_,
diagnostic_updater::FrequencyStatusParam(&min_freq, &max_freq, 0.1, 10),
diagnostic_updater::TimeStampStatusParam(-1, 0.5));
// 발행하면 자동으로 주파수·스탬프가 감시된다
diag_pub_.publish(msg);
견해: 센서 드라이버에는
DiagnosedPublisher를 기본으로 쓰라. 코드 몇 줄로 주파수·타임스탬프 감시가 붙고, 이 둘이 센서 문제의 상당 부분을 조기에 검출한다. "카메라가 가끔 느려진다"를 사후가 아니라 발생 시점에 알 수 있다.
6.9.5 집계기
노드가 많아지면 진단 항목이 수십~수백 개가 된다. 그대로는 볼 수 없다.
# config/diagnostics.yaml
analyzers:
ros__parameters:
path: 로봇
drive:
type: diagnostic_aggregator/GenericAnalyzer
path: 구동계
contains: ['모터', 'wheel']
timeout: 5.0
sensors:
type: diagnostic_aggregator/GenericAnalyzer
path: 센서
startswith: ['lidar', 'camera', 'imu']
timeout: 5.0
num_items: 3 # 이만큼 없으면 STALE
compute:
type: diagnostic_aggregator/GenericAnalyzer
path: 연산
contains: ['CPU', '메모리', '온도']
timeout: 5.0
집계기가 하는 일:
| 기능 | 설명 |
|---|---|
| 계층화 | 경로 기반으로 트리 구성 |
| 최악값 전파 | 하위에 ERROR가 있으면 상위도 ERROR |
| STALE 판정 | timeout 안에 갱신 없으면 |
| 필수 항목 검사 | num_items로 누락 탐지 |
견해:
num_items가 과소평가된 기능이다. 센서 3개가 있어야 하는데 2개만 보고하면, 나머지 하나의 노드가 전혀 기동되지 않았다는 뜻이다. "없는 것"을 감지하는 것이 "이상 상태"을 감지하는 것만큼 중요하다.
6.9.6 실시간 경로와의 연결
이 책 전체에서 반복한 패턴의 완성이다.
// 실시간 경로 (6.2.7 · 6.4.4 · 6.6.3)
hardware_interface::return_type MyHardware::read(
const rclcpp::Time &, const rclcpp::Duration & period)
{
++comm_total_; // 원자적, relaxed면 충분
if (!communicate()) {
++comm_errors_;
return hardware_interface::return_type::OK; // 임계 전까지는 OK
}
// 주기 초과 감시 (6.6.8)
if (period.seconds() > period_limit_) ++overruns_;
// 최대값 갱신 — CAS 루프 없이 근사해도 충분
const double rtt = measure_rtt_ms();
double prev = rtt_max_ms_.load(std::memory_order_relaxed);
if (rtt > prev) rtt_max_ms_.store(rtt, std::memory_order_relaxed);
return hardware_interface::return_type::OK;
}
왜 이 구조인가:
| 이유 | 설명 |
|---|---|
| 실시간 경로에 로깅·할당이 없다 | 원자적 정수 연산만(6.6.3) |
| 진단 태스크는 비실시간 | 문자열·발행 자유 |
| 카운터 리셋으로 구간 통계 | 누적값보다 유용 |
| 레벨 판정을 한 곳에서 | 임계 관리 용이 |
견해:
exchange(0)로 구간 통계를 만드는 것이 누적 카운터보다 낫다. "총 오류 12345개"는 정보가 적지만 "최근 1초에 3개"는 즉시 판단 가능하다. 추세를 보려면 시계열로 기록하면 되고(11.3), 그것도 구간 값이 있어야 한다.
함정:
memory_order_relaxed로 충분한지 판단하라. 단순 카운터는 relaxed로 충분하지만(2.9.4), 여러 값 사이의 일관성이 필요하면 다르다. 진단은 근사값이어도 되는 경우가 대부분이므로 relaxed가 적절하다.
6.9.7 설계 원칙 — 무엇을 어느 레벨로
레벨 판단 기준:
| 레벨 | 기준 | 예 |
|---|---|---|
OK | 정상 범위 | 오류율 0.1%, 온도 45℃ |
WARN | 추세가 악화됐거나 여유가 줄었다 | 오류율 2%, 온도 70℃, 배터리 20% |
ERROR | 기능이 실패했거나 곧 실패한다 | 통신 두절, 온도 85℃, 배터리 5% |
핵심: WARN은 "아직 동작하지만 조치가 필요하다" 를 의미한다. 이것이 예방 정비의 근거가 된다.
견해:
WARN임계를 의미 있게 설정하는 것이 진단의 가치를 좌우한다. 너무 민감하면 상시 노란색이라 아무도 안 보고, 너무 둔감하면 ERROR와 동시에 출력된다. 정상 운영 데이터를 수집해 통계적으로 정하는 것이 좋다 — 평소 오류율이 0.1%라면 1%가 의미 있는 WARN이다.
보고할 항목 선정:
| 범주 | 항목 |
|---|---|
| 통신 | 오류율·재연결·왕복 시간(6.7.9) |
| 타이밍 | 주기 지터·초과 횟수(6.6.6) |
| 하드웨어 | 온도·전압·전류·모터 오류 코드 |
| 데이터 흐름 | 발행 주파수·데이터 나이(6.9.4) |
| 처리 | 큐 길이·드롭 수·처리 지연 |
| 알고리즘 | tf 실패율(5.2.8)·측위 신뢰도·계획 실패율 |
| 자원 | CPU·메모리·디스크(1.11.3의 로그 디스크) |
| 안전 | 비상 정지 상태·한계 도달·워치독 |
보고하지 말아야 할 것:
| 항목 | 이유 |
|---|---|
| 고빈도 원시 데이터 | 진단은 요약이지 데이터 스트림이 아니다 |
| 비밀 값 | 진단은 기록·전송된다(1.11.8) |
| 매번 바뀌는 무의미한 값 | 노이즈 |
견해: 진단은 "이 노드가 건강한가"에 답해야 한다. 내부 상태를 전부 노출하는 것이 아니라, 운영자가 조치를 결정할 수 있는 정보를 주는 것이 목적이다. 상세 디버깅 정보는 로그·별도 토픽으로 분리한다.
계층적 요약:
void diagnose_overall(diagnostic_updater::DiagnosticStatusWrapper & stat)
{
// 하위 상태를 종합한 한 줄 — 운영자가 처음 보는 것
if (!comm_ok_) stat.summary(ERROR, "통신 두절 — 즉시 점검");
else if (temp_ > 80.0) stat.summary(ERROR, "과열 — 정지 권장");
else if (error_rate_ > 2.0) stat.summary(WARN, "통신 불안정 — 케이블 점검");
else stat.summary(OK, "정상 동작");
}
견해: 요약 메시지에 "무엇을 해야 하는가"를 넣으라. "온도 85℃"보다 "과열 — 정지 권장"이 낫다. 현장 운영자는 엔지니어가 아니며, 조치를 지시하는 메시지가 실제로 도움이 된다(1.11.8의 "ERROR는 행동을 시사해야 한다"와 같은 원칙).
6.9.8 운영 활용
시각화:
# GUI 모니터 (계층 표시)
ros2 run rqt_robot_monitor rqt_robot_monitor
# 원시 확인
ros2 topic echo /diagnostics
ros2 topic echo /diagnostics_agg
# 특정 항목만
ros2 topic echo /diagnostics --field status[0].message
기록:
# 진단을 bag에 함께 기록 (11.3)
ros2 bag record /diagnostics /diagnostics_agg /rosout /tf ...
견해:
/diagnostics를 bag에 항상 포함시키라. 1.11.9에서/rosout을 함께 기록하라고 한 것과 같은 이유다 — 사후 분석에서 "그 시점에 시스템이 어떤 상태였나"를 데이터와 나란히 볼 수 있다. 저빈도·소형이라 비용도 낮다.
경보 연동:
# 진단을 감시해 외부로 알리는 노드 (개념)
class DiagnosticAlerter(Node):
def __init__(self):
super().__init__('diagnostic_alerter')
self.sub = self.create_subscription(
DiagnosticArray, '/diagnostics_agg', self.on_diag, 10)
self.last_level = {}
def on_diag(self, msg):
for status in msg.status:
prev = self.last_level.get(status.name, 0)
# 레벨이 높아진 순간에만 알림 (상시 알림 방지)
if status.level > prev and status.level >= DiagnosticStatus.ERROR:
self.send_alert(status)
self.last_level[status.name] = status.level
함정: 매 진단 메시지마다 알림을 보내면 알림 폭주가 된다. 1 Hz로 발행되므로 ERROR 상태가 1분 지속되면 60번 알린다. 상태 전이 시점에만 알리고, 지속되면 주기적 요약으로 전환하는 것이 실용적이다.
플릿 운영(14.9):
| 활용 | 설명 |
|---|---|
| 중앙 대시보드 | 여러 로봇의 진단을 한 화면에 |
| 예방 정비 | WARN 추세로 부품 교체 시점 판단 |
| 통계 분석 | 어떤 부품이 자주 문제인가 |
| 가용성 지표 | ERROR 시간 비율 |
6.9.9 진단 없는 시스템의 증상
진단이 왜 필요한지를 반대로 보면 명확하다.
| 증상 | 진단이 있었다면 |
|---|---|
| "로봇이 가끔 멈춘다" | 통신 오류율 추세로 케이블 문제 조기 발견 |
| "왜 느려졌는지 모르겠다" | 주기 초과·CPU 지표 |
| "센서가 언제부터 이상했나" | 주파수 감시 기록 |
| "이 노드가 종료된 것을 몰랐다" | STALE 감지 |
| "현장에서 원인을 못 찾는다" | 하드웨어 오류 코드 노출 |
| "예방 정비 시점을 모른다" | WARN 추세 |
견해: 진단은 개발 막바지에 추가하는 것이 아니라 드라이버·컨트롤러를 쓸 때 함께 만드는 것이다. 나중에 추가하려면 실시간 경로에 카운터를 추가하고 구조를 바꿔야 한다. 6.2·6.4·6.7에서 카운터를 미리 두라고 한 이유가 이것이며, 그 카운터들이 이 장에서 값을 한다.
6.9.12 Updater의 주기와 이름 — 실측
앞 절의 예제가 setPeriod(1.0)과 setHardwareID(...)를 호출했다. 그 값들이 실제로 무엇보다 후순위이고 무엇보다 우선하는지를 Jazzy에서 컴파일해 확인했다(실행 확인). 둘 다 "코드에 썼는데 적용되지 않는다"가 나오는 자리다.
6.9.12.1 갱신 주기는 네 곳에서 정해진다
Updater는 갱신 주기를 ROS 파라미터로도 받는다 — 이름은 diagnostic_updater.period다. 그래서 우선순위가 생긴다. 실제로 측정해 본 결과는 이렇다:
| 상황 | getPeriod() 결과 |
|---|---|
| 아무것도 안 함 | 1.0초(기본값) |
생성자 인자만 5.0 | 5.0초 |
파라미터 0.2 + 생성자 인자 5.0 | 0.2초 — 파라미터가 우선한다 |
위 상태에서 setPeriod(3.0) 호출 | 3.0초 — 명시 호출이 우선한다 |
정리하면 setPeriod() 호출 > diagnostic_updater.period 파라미터 > 생성자 인자 > 기본값 1.0초다.
함정: 생성자 인자는 파라미터에 진다.
Updater updater(node, 5.0);이라고 써 두고 YAML에diagnostic_updater.period: 0.1이 있으면, 진단이 초당 열 번 나간다. 코드만 읽어서는 이유를 알 수 없고, 헤더 주석에만 적혀 있다. 주기를 코드로 못 박고 싶으면 생성자 인자가 아니라setPeriod()를 호출하라 — 그것만이 파라미터에 우선한다.
견해: 반대로, 주기를 파라미터로 두는 편이 대체로 낫다. 진단 주기는 현장에서 조정하고 싶어지는 값이고(대역이 빠듯하면 늘리고, 문제를 쫓을 땐 줄인다), 이 설계는 그것을 코드 수정 없이 가능하게 하려는 의도로 보인다. 코드로 명시하는 것은 주기가 안전 논리의 일부일 때로 한정하라.
6.9.12.2 진단 이름에 네임스페이스가 안 들어간다 — 다중 로봇의 함정
같은 파라미터 그룹에 diagnostic_updater.use_fqn이 하나 더 있다. 진단 항목의 name 필드 형식을 정한다. 네임스페이스 /robot 아래 probe 노드에서 배터리 항목을 발행해 실제 메시지를 받아 봤다:
use_fqn | 발행되는 name |
|---|---|
false(기본값) | probe: 배터리 |
true | /robot/probe: 배터리 |
기본값이 문제다. 로봇 여러 대를 네임스페이스로 나눠 운영하면(1.1.7) /robot1/probe와 /robot2/probe가 둘 다 probe: 배터리라는 같은 이름으로 진단을 낸다. 집계 화면에서 어느 로봇의 배터리인지 구분되지 않고, 두 로봇의 상태가 한 줄로 겹쳐 보인다.
# 다중 로봇이면 켜라
/**:
ros__parameters:
diagnostic_updater:
use_fqn: true
함정:
setHardwareID로 대신하려 하지 마라.hardware_id는DiagnosticStatus의 별도 필드이고, 대시보드·집계기는 보통name으로 항목을 식별한다. 하드웨어 ID를 잘 넣어 두어도 이름이 겹치면 화면에서는 여전히 겹친다. 둘은 보완재지 대체재가 아니다.
함정:
use_fqn을 나중에 켜면 이름이 전부 바뀐다. 진단 이름을 키로 삼아 만든 알림 규칙·대시보드 위젯·집계기 설정이 한꺼번에 어긋난다. 다중 로봇이 될 가능성이 있다면 한 대일 때 미리 켜 두는 것이 비용이 낮다.
6.9.14 진단을 워치독으로 써 봤다 — 실측
14.8.13.2은 "systemd가 볼 수 없는 실패가 있으니 상태 감시를 따로 두라"로 끝났다. 그 상태 감시를 진단 체계로 만들 수 있는지 확인했다(실행 확인, Jazzy — diagnostic_updater·diagnostic_aggregator 4.2.7).
구성: 노드가
diagnostic_updater로 자기 라이프사이클 상태를 보고하고,diagnostic_aggregator의GenericAnalyzer가 그것을 묶는다. 세 상황에서/diagnostics_agg의 최상위 판정을 봤다.
6.9.14.1 결과 — 두 종류의 실패를 모두 검출한다
| 상황 | 개별 항목 | /Robot/Arm | /Robot(최상위) |
|---|---|---|---|
| 정상 | OK · active | OK | OK |
| 노드가 종료돼 침묵 | active(마지막 값 그대로) | Stale | Stale |
실행 중이나 finalized 보고 | Error · finalized — 복구 불가 | Error | Error |
6.9.2의 주장이 확인된다 — "STALE은 노드가 보고한 것이 아니라 집계기가 판단하는 것"이 그대로다. 노드를 강제 종료하자 timeout(3초) 뒤 상위가 Stale로 바뀌었다.
그리고 14.8.13.2의 감시 공백이 메워진다.
| systemd가 못 보는 실패 | 진단 체계는 | 조건 |
|---|---|---|
프로세스가 종료됐는데 on-failure가 안 잡음(SIGTERM) | Stale로 검출한다 | 진단 발행이 끊기므로 자동 |
finalized 노드가 실행 중(1.12.10.3) | Error로 검출한다 | 노드가 상태를 보고해야 한다 |
둘째 줄이 이 절의 요점이다.
finalized노드는 계속 살아서 진단도 계속 발행한다 — 그러면 집계기는Stale을 못 낸다. 라이프사이클 상태를 진단에 실어 나르지 않으면 그 실패는 여기서도 안 보인다.그래서 관리형 노드의 진단 태스크에는 자기 상태를 넣어야 한다.
up.add("lifecycle", [this](DiagnosticStatusWrapper & s) {
auto st = get_current_state().label(); // active / finalized / …
if (st == "active") s.summary(s.OK, st);
else if (st == "unconfigured") s.summary(s.WARN, st);
else s.summary(s.ERROR, st + " — 복구 불가");
s.add("state", st);
});
6.9.14.2 개별 항목만 보면 종료된 것을 모른다
위 표의 둘째 줄에서 개별 항목의 message는 여전히 active다 — 마지막으로 받은 값이 그대로 남고, 상위 그룹만 Stale로 바뀐다.
함정: 대시보드나 스크립트가 개별 항목만 읽으면 종료된 노드를 정상으로 본다. 판정은 그룹 수준(
/Robot·/Robot/Arm)에서 읽어야 한다. 6.9.5의 계층 구조가 장식이 아니라 판정의 단위인 이유다.
6.9.14.3 분석기가 아무것도 검출하지 못하면 그것이 장애로 보인다
startswith를 잘못 적어 분석기가 한 항목도 매칭하지 못한 상태로 실행해 봤다.
| 항목 | 결과 |
|---|---|
잘못 설정한 분석기 /Robot/Arm | Stale |
최상위 /Robot | Stale |
실제 진단은 catch-all(/Robot/Other)로 흘러가 | OK |
빈 분석기가 Stale이 되고 최상위까지 끌어내린다. 진단은 정상적으로 오고 있는데 설정 오류가 시스템 장애처럼 보인다.
진단 이름의 형식이 여기서 중요하다. 원시 진단의 이름은
<노드 이름>: <태스크 이름>이다 — 이 실험에서는diagnode: lifecycle이었다(6.9.12의 이름 규칙).startswith: ["lifecycle"]처럼 태스크 이름으로 적으면 안 맞는다.startswith: ["diagnode"]처럼 노드 이름으로 시작해야 한다.증상이 혼동을 일으킨다 — "집계기를 켰더니 전체가 Stale"이면 노드를 의심하기 전에 분석기 매칭을 보라.
/diagnostics(원시)에 항목이 있는데/diagnostics_agg에서Other로 빠져 있다면 매칭 실패다.
6.9.14.4 부수로 확인한 것 — 헤더 경로
diagnostic_updater의 헤더는 include/diagnostic_updater/diagnostic_updater.hpp 로, 패키지 이름이 한 번만 나온다. 대부분의 ROS 2 패키지가 include/<pkg>/<pkg>/… 로 두 번 중첩되는 것과 다르다.
# 대부분: include/rclcpp/rclcpp/…
# diagnostic_updater: include/diagnostic_updater/diagnostic_updater.hpp
직접 g++로 빌드할 때 -I<prefix>/include(기본 경로)가 필요하다. ament_cmake를 쓰면 드러나지 않지만, 도구를 만들거나 수동 빌드할 때 걸린다(4.2).
6.9.14.5 확인하지 못한 것
timeout값에 따른Stale전환 지연은 정밀하게 측정하지 않았다 — 3초로 두고 "몇 초 뒤 바뀐다" 수준만 봤다(확인 필요).AnalyzerGroup의 중첩 계층(그룹 안 그룹)은 시험하지 않았다.- 진단을 받아 실제로 프로세스를 강제 종료하는 워치독 노드는 만들지 않았다 — 이 실험은 판정까지다
(확인 필요: 조치 자동화).
6.9.10 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | Updater를 멤버로 미보관 | 진단 미발행 | 멤버 보관 |
| 2 | 태스크에서 하드웨어 직접 읽기 | executor 블로킹 | 수집된 값만 보고 |
| 3 | 실시간 경로에서 진단 발행 | 지터(6.6.3) | 카운터 + 별도 태스크 |
| 4 | 누적 카운터만 보고 | 판단 어려움 | exchange(0) 구간 통계 |
| 5 | WARN 임계가 부적절 | 상시 노란색·무시됨 | 운영 데이터로 설정 |
| 6 | 요약에 조치가 없음 | 운영자가 판단 못 함 | "무엇을 하라"를 명시 |
| 7 | hardware_id 미설정 | 여러 대 중 어느 것인지 모름 | 설정 |
| 8 | 집계기 num_items 미사용 | 없는 노드를 못 잡음 | 필수 항목 선언 |
| 9 | 진단을 bag에 미포함 | 사후 분석 불가 | 항상 기록 |
| 10 | 매 메시지마다 알림 | 알림 폭주 | 상태 전이 시점만 |
| 11 | 원시 데이터를 진단으로 | 대역·가독성 저하 | 요약만 |
| 12 | 비밀 값 노출 | 보안(13.4) | 마스킹 |
| 13 | 개발 막바지에 추가 | 구조 변경 필요 | 처음부터 함께 |
| 14 | 다중 로봇에서 진단 이름 충돌 | 두 로봇이 같은 probe: 항목 | diagnostic_updater.use_fqn: true(6.9.12.2) |
| 15 | 생성자 인자로 주기 고정 | 파라미터가 우선한다 | setPeriod() 호출(6.9.12.1) |
| 16 | 집계기 켠 뒤 전체가 Stale | 분석기 매칭 실패 — 빈 분석기가 Stale | 이름은 <노드>: <태스크>(6.9.14.3) |
| 17 | 개별 항목만 읽어 건강 판정 | 종료돼도 마지막 값이 남는다 | 그룹 수준에서 읽어라(6.9.14.2) |
| 18 | 관리형 노드가 자기 상태를 진단에 안 넣음 | finalized가 안 보인다 | 라이프사이클을 태스크로(6.9.14.1) |
6.9.11 체크포인트
- 진단 레벨 네 가지와 각각의 의미를 말할 수 있는가.
STALE이 특별한 이유는? message와values의 역할 분담을 설명할 수 있는가.diagnostic_updater의 기본 구성 요소 다섯 가지를 말할 수 있는가. 태스크 콜백에서 하드웨어를 직접 읽으면 안 되는 이유는?- 주파수 감시와 타임스탬프 감시가 각각 무엇을 조기에 검출하는가.
DiagnosedPublisher를 센서 드라이버에 기본으로 쓰라는 조언의 근거는?- 집계기의 기능 네 가지를 말할 수 있는가.
num_items가 검출하는 것은? - 실시간 경로와 진단 태스크를 잇는 패턴을 그릴 수 있는가. 왜 이 구조여야 하는가.
exchange(0)구간 통계가 누적 카운터보다 나은 이유는?WARN임계를 정하는 방법과, 잘못 정했을 때의 두 가지 실패 양상은?- 진단으로 보고할 항목 여섯 범주와, 보고하지 말아야 할 것 세 가지를 말할 수 있는가.
- 요약 메시지에 조치를 넣으라는 조언이 1.11.8의 어떤 원칙과 같은가.
- 진단을 bag에 포함시켜야 하는 이유는?
- 알림 폭주를 막는 방법은?
Updater의 갱신 주기를 정하는 네 경로의 우선순위는? 다중 로봇에서 진단 이름이 충돌하는 이유와 대처는(6.9.12).- 노드가 종료됐을 때 개별 항목과 그룹 항목은 각각 어떻게 되는가(6.9.14.2).
finalized노드를 진단으로 검출하려면 무엇이 필요한가(6.9.14.1).- 집계기를 켰더니 전체가 Stale이면 무엇을 먼저 의심하는가(6.9.14.3).
여기까지가 제6부 하드웨어 인터페이스다. ros2_control 아키텍처(6.1), 하드웨어 컴포넌트(6.2), 관리 층(6.3), 컨트롤러 작성(6.4)과 표준 카탈로그(6.5), 실시간 제약(6.6), 버스와 드라이버(6.7), MCU 통합(6.8), 그리고 진단(6.9).
제6부가 반복한 것은 세 가지다.
- 경계를 긋는다. 하드웨어 통신과 제어 알고리즘을 분리하고(6.1.2), 실시간 경로와 ROS 통신을 분리하고(6.1.6), 빠른 것은 MCU에 느린 것은 ROS에 둔다(6.8.8).
- 활성화 순간이 위험하다. 하드웨어 컴포넌트도(6.2.6) 컨트롤러도(6.4.7) 활성화 시 현재 상태로 초기화하지 않으면 로봇이 급격히 움직인다. 시뮬에서는 안 드러나고 실기에서 물리적 사고가 된다.
- 조용한 실패를 드러낸다. 실시간 경로에서는 로그를 못 쓰므로 카운터를 두고(6.6.3), 그것을 진단으로 내보내(6.9) 시스템이 조용히 악화되는 것을 감지한다.
이제 로봇은 움직인다. 다음은 본다. 카메라·라이다·IMU가 만드는 데이터를 어떤 규약으로 표현하고, 어떻게 처리하며, 서로 다른 시각의 센서를 어떻게 정렬하는가 — 제7부 센서·인지 파이프라인(7.1~7.8) 의 주제다. 센서 메시지 규약(REP)부터 시작한다.
6.9.13 제6부를 마치며 — 하드웨어 인터페이스
이 장은 제6부(6.1~6.9)의 마지막이므로, 부 전체를 하나의 관점으로 묶는다.
제6부가 반복해 말한 세 가지:
- 제어 루프는 예산이 있다. 주기 안에 끝나야 하고, 못 끝내면 조용히 성능이 떨어진다(6.2·6.6). 여유가 아니라 최악을 재라.
- 하드웨어 경계는 상태기계다. 활성화·비활성화·오류 복구를 설계하지 않으면 재시작 말고는 방법이 없는 시스템이 된다(6.3·6.4).
- 타입과 권한이 조용히 어긋난다.
TwistStamped인지Twist인지(6.5.12), RT 권한이 실제로 붙었는지(12.2.12.4) — 로그 한 줄이 성패를 가른다.
다음 부에서 무엇이 달라지나: 제6부는 명령을 내보내는 쪽이었다. 제7부는 들어오는 쪽 — 센서 데이터다. 대역·지연·동기화가 지배 요인이 되고, "어떻게 다 처리할 것인가"가 아니라 "무엇을 버릴 것인가"가 설계 문제가 된다.
제7부는 센서·인지 파이프라인(7.1~7.8)다.