요구사항 명세(SR) 학습 노트 목차

07. 모바일 인터페이스 — 요청을 만들 뿐, 실행을 보장하지 않는다

출처: SR §6.5 (6.5.1 인터페이스 역할 및 사용자 요청 처리 / 6.5.2 원격 사용자 인증 및 통신 안전 / 6.5.3 차량 상태 및 오류 정보의 공통 표시 원칙 / 6.5.4 모바일 인터페이스 공통 기능), §6.6 (기능별 상태 및 사용자 알림) 관련 TBD: TBD-001(Central ControllerESP32 물리 통신), TBD-021(HMI 상태 갱신 재전송), TBD-022(센싱/Vision 데이터 최대 age), TBD-027(차량 전원 상태 생성 주체 및 입력 방식) 담당 노드: ESP32(차량 내 HMI 디스플레이 + 모바일 앱 연계 + BLE Digital Key)


0-bis. 차량 내 HMI 를 없앴다 — 무엇이 달라지나

2026-09 결정: 차량 내 디스플레이(HMI)를 만들지 않고 모바일 인터페이스만 둔다.

SR v1.0 은 곳곳에서 **"차량 내 HMI 및 모바일 인터페이스"**를 나란히 쓴다. HMI 가 빠지면 그 문장들이 전부 영향을 받으므로, §9 의 변경 영향 분석을 먼저 돌려야 한다.

모바일로 넘어오는 요구사항

SR원문이 요구한 주체이관 후
§6.1.4"차량 내 디스플레이(HMI)에 VSS 오류 상태를 전달하여 표시"모바일이 받는다
§6.2.6"HMI 에서 윈도우 7상태·거부 사유를 사용할 수 있도록"모바일
§6.3.1"HMI 는 유효한 센서 값·탑승자 판정을 제공"모바일
§6.3.3"차량 내 HMI 는 후방 근접 위험을 표시·경고"문제 — 아래 참고
§6.3.5"차량 내 HMI 는 엔진룸 동물 진입 상태를 표시"모바일
§6.4.x각 기능의 설정·상태 확인모바일
§6.5.3공통 표시 원칙 (stale 금지·오류 3종 구분)모바일
§6.6"HMI 는 기능별 동작 상태를 제공"모바일

모바일이 유일한 사용자 인터페이스가 되므로, 이 편의 요구사항이 사실상 전 기능의 사용자 접점을 떠안는다.

그런데 그대로 옮길 수 없는 것이 있다

§6.3.3 후방 근접 위험 경고가 문제다.

"차량 내 HMI 는 후방 근접 위험 상태가 유효한 경우 탑승자에게 경고해야 한다."

탑승자는 폰을 보고 있지 않다. 후진 중에 앱 화면을 볼 리가 없고, 봐서도 안 된다. 이건 모바일로 이관하면 요구사항의 의도가 깨진다.

남는 경로는 VSS 음향뿐이다. §6.1.1 이 이미 초음파 장애물 경고음을 정의하고 있으므로 기능 자체는 살아 있지만, 시각 표시가 사라진다는 것을 명시적으로 기록해야 한다.

같은 성격의 것이 하나 더 있다 — §6.1.4 의 알림 채널 이중화다.

원래   VSS 자체 오류(소리를 못 냄)  →  HMI 화면으로 알린다
지금   VSS 자체 오류               →  모바일로 알린다
                                      ↑ 사용자가 앱을 안 켜고 있으면?

차 안에 있는 사용자에게 즉시 알릴 수단이 없어졌다. Ambient Lighting 이 유일한 차내 시각 채널로 남는다.

그래서 SR 에 반영해야 할 것

① §6.3.3 의 "차량 내 HMI 는 … 탑승자에게 경고" 를 어떻게 할 것인가
     - VSS 음향으로만 간다고 명시하거나
     - Ambient Lighting 에 근접 경고 표시를 추가하거나
② §6.1.4 의 VSS 자체 오류 통지 경로를 재정의
③ SR 전반의 "차량 내 HMI 및 모바일 인터페이스" 를 "모바일 인터페이스" 로 정정
④ 시험 항목에서 HMI 표시 검증 항목을 모바일로 옮기거나 삭제

①은 SysRS 가 임의로 정할 게 아니라 SR 작성자 확인이 필요하다. 안전 관련 경고의 채널이 줄어드는 문제라서 그렇다.

ESP32 는 그대로 남는다. 화면이 없어졌을 뿐 BLE·Wi-Fi 게이트웨이 역할은 유지된다.


0. 이 절의 위치

§6.1~§6.4는 각 기능이 무엇을 하는가를 정의했다. §6.5~§6.6은 그 결과가 사용자에게 어떻게 도달하는가를 정의한다.

다이어그램 로딩 중…

양방향이다. 상태는 위로 올라오고, 요청은 아래로 내려간다. 그리고 §6.5의 첫 요구사항이 이 두 방향의 관계를 규정한다.


1. §6.5.1 — 인터페이스 역할 및 사용자 요청 처리

  • 차량 내 HMI 및 모바일 인터페이스는 허용된 차량 기능에 대한 사용자 제어 요청을 생성할 수 있어야 한다.
  • 차량 내 HMI 및 모바일 인터페이스는 사용자 제어 요청의 접수, 거부, 진행, 완료, 중단 또는 실패 결과를 사용자에게 제공해야 한다.
  • 차량 내 HMI 및 모바일 인터페이스는 제어 요청이 거부되거나 실패한 경우 그 사유를 사용자에게 제공해야 한다.
  • 차량 내 HMI 및 모바일 인터페이스는 기능 제어 요청을 생성할 수 있으나, 차량 기능의 실제 수행 여부는 해당 기능의 허용 조건 및 안전 정책에 따라 결정되어야 한다.

1-1. 마지막 문장이 이 시스템 아키텍처의 요약이다

"차량 내 HMI 및 모바일 인터페이스는 기능 제어 요청을 생성할 수 있으나, 차량 기능의 실제 수행 여부는 해당 기능의 허용 조건 및 안전 정책에 따라 결정되어야 한다."

요청 생성 권한 ≠ 실행 권한. 이걸 명시적으로 못 박은 이유는, HMI가 "명령"을 보낸다고 생각하면 설계가 무너지기 때문이다.

✗ 잘못된 멘탈 모델:  HMI 는 명령한다 → 기능은 수행한다
✓ 올바른 멘탈 모델:  HMI 는 요청한다 → 기능이 판단한다 → 결과를 HMI 에 알린다

02편의 윈도우 6단계 관문이 이걸 실증한다. HMI가 "닫힘"을 요청해도 Anti-pinch 사용 불가면 거부된다. 04편의 도어락도 마찬가지 — 문이 열려 있으면 잠금 요청이 거부된다.

따라서 HMI는 "요청 후 상태 갱신을 기다린다"는 모델로 구현해야 한다.

/* ESP32 HMI — 낙관적 UI 업데이트를 하면 안 된다 */

/* ✗ 금지 — 버튼을 누르는 순간 상태를 바꿔 그린다 */
void on_lock_button(void) {
    send_lock_request();
    ui_set_door_state(UI_LOCKED);      /* 실제로 잠겼는지 모른다 */
}

/* ✓ 요청은 요청, 표시는 실제 상태 */
void on_lock_button(void) {
    uint16_t req_id = send_lock_request();
    ui_set_request_state(req_id, REQ_SENT);    /* "요청 보냄"을 표시 */
    /* 도어 상태는 차량이 알려줄 때까지 그대로 둔다 */
}

void on_request_result(uint16_t req_id, req_result_t r, reject_reason_t why) {
    ui_set_request_state(req_id, r);
    if (r == REQ_REJECTED || r == REQ_FAILED) ui_show_reason(why);   /* §6.5.1 */
}

낙관적 UI(optimistic UI)는 웹 앱에서는 좋은 패턴이지만 차량 제어에서는 위험하다. 사용자가 "잠김"을 보고 떠났는데 실제로는 안 잠겼을 수 있다.

1-2. 요청 생애주기 6단계

"접수, 거부, 진행, 완료, 중단 또는 실패"

02편의 §6.2.6이 "수락, 진행, 완료, 거부 또는 중단"으로 5개였는데, 여기는 "실패"가 추가돼 6개다.

상태의미중단과의 차이
접수(수락)유효성·우선순위 통과, 실행 대기/시작
거부조건 미충족으로 시작하지 않음시작 자체를 안 함
진행실행 중
완료목표 상태 도달 확인
중단외부 요인으로 멈춤 (상위 요청, 안전 개입)의도된 개입
실패실행했으나 목표 도달 못 함의도치 않은 결과

중단과 실패의 구분이 중요하다.

  • Anti-pinch로 창문이 멈춤 → 중단 (시스템이 의도적으로 개입, 정상 동작)
  • 도어락 구동했으나 타임아웃 → 실패 (원인 불명, 고장 가능성)

사용자 대응이 다르다. 중단은 "다시 시도하세요", 실패는 "점검이 필요합니다".

typedef enum {
    REQ_ACCEPTED, REQ_REJECTED, REQ_IN_PROGRESS,
    REQ_COMPLETED, REQ_ABORTED, REQ_FAILED,
} req_result_t;

/* 요청 하나를 추적하는 최소 구조 */
typedef struct {
    uint16_t        id;
    uint8_t         function;      /* 어느 기능에 대한 요청인가 */
    req_result_t    state;
    reject_reason_t reason;        /* REJECTED / FAILED 시 사유 (§6.5.1) */
    uint32_t        created_ms;
} req_track_t;

1-3. "허용된 차량 기능에 대한" 요청만 생성한다

첫 문장의 수식어다. HMI가 임의의 요청을 만들 수 있는 게 아니라, 허용된 기능 목록 안에서만 만든다.

이건 보안 관점에서 중요하다. ESP32Wi-Fi/BLE로 외부에 노출된 노드다. 공격자가 ESP32를 장악했을 때, 그 노드가 만들 수 있는 요청의 범위가 곧 피해 범위가 된다. 화이트리스트 방식이어야 한다.


2. §6.5.2 — 원격 사용자 인증 및 통신 안전

  • 시스템은 인증 및 차량별 권한 확인이 완료된 모바일 사용자에게만 원격 차량 제어 기능을 제공해야 한다.
  • 시스템은 모바일 인터페이스와 차량 제어 기능 사이의 통신 정보를 신뢰할 수 없는 경우 새로운 차량 제어 요청을 허용하지 않아야 한다.
  • 시스템은 모바일 연결이 종료된 경우 이전 사용자 요청을 자동으로 재전송하지 않아야 한다.
  • 모바일 인터페이스는 원격 차량 제어 요청의 접수 여부, 최종 처리 결과 및 거부 사유를 사용자에게 표시해야 한다.

2-1. 인증과 권한은 별개다

"인증차량별 권한 확인이 완료된 모바일 사용자에게만"

무엇을 확인하나실패하면
인증(authentication)이 사람이 누구인가신원 불명 → 거부
권한(authorization)이 사람이 이 차량에 대해 무엇을 할 수 있는가남의 차 → 거부

"차량별"이라는 단어가 핵심이다. 인증만 확인하면, 로그인한 아무 사용자나 아무 차량을 조작할 수 있다. 차량과 사용자의 바인딩이 별도로 검증돼야 한다.

사용자 A 로그인 성공 (인증 ✓)
  → 차량 X 에 대한 권한이 있는가? (권한 확인)
      → 소유자? 공유받은 사용자? 권한 만료?

§6.3.5에도 같은 표현이 나온다 — "권한이 있는 원격 사용자에게 즉시 알려야 한다". 알림 대상도 권한으로 필터링된다.

2-2. "자동으로 재전송하지 않아야 한다"

"시스템은 모바일 연결이 종료된 경우 이전 사용자 요청을 자동으로 재전송하지 않아야 한다."

00편 패턴 C의 통신 버전이다. 네트워크 재시도 로직이 물리 동작을 두 번 일으키는 것을 막는다.

시나리오:

사용자가 앱에서 "잠금" 탭
  → 요청 전송, 응답 대기
  → 연결 끊김 (지하주차장 진입)
  → 앱이 "실패"로 표시
  → 사용자가 다시 탭
  → 연결 복구되면서 이전 요청도 함께 도착
  → 잠금 명령 2회

도어락이 두 번 구동되는 건 소음과 마모 문제로 끝나지만, 만약 토글(toggle) 방식 명령이었다면 잠금 → 해제가 되어 보안 사고다.

방어는 두 겹이다.

/* ① 요청 측 — 자동 재전송 금지 (§6.5.2) */
static void on_connection_lost(void) {
    pending_requests_clear();       /* 대기 중인 요청을 버린다 — 재전송하지 않는다 */
    ui_show_all_pending_as_unknown();  /* 결과를 모른다고 표시 */
}

/* ② 수신 측 — 멱등성 (§6.2.1, §6.4.1) */
static bool request_is_duplicate(uint16_t req_id) {
    for (int i = 0; i < RECENT_N; i++)
        if (g_recent_ids[i] == req_id) return true;
    return false;
}

요청 ID 기반 중복 제거가 있으면 재전송이 발생해도 안전하다. 하지만 SR은 애초에 자동 재전송을 금지하는 쪽을 택했다 — 더 단순하고, 사용자가 상황을 인지한 상태에서 다시 시도하게 만든다.

2-3. "결과를 모른다"를 표시해야 한다

마지막 문장이 요구하는 것:

"모바일 인터페이스는 원격 차량 제어 요청의 접수 여부, 최종 처리 결과거부 사유를 사용자에게 표시해야 한다."

세 가지가 각각 다른 정보다.

접수 여부      : 차량이 요청을 받았는가?          (통신 성공 여부)
최종 처리 결과  : 요청이 어떻게 끝났는가?          (완료/거부/중단/실패)
거부 사유      : 왜 거부됐는가?                  (원인)

"접수 여부"가 따로 있는 이유: 연결이 끊기면 요청이 도달했는지조차 모른다. 이때 "실패"라고 표시하면 거짓말이다 — 실제로는 성공했을 수도 있다. **"전송했으나 결과 확인 불가"**라는 상태가 UI에 있어야 한다.

typedef enum {
    RREQ_SENDING,        /* 전송 중 */
    RREQ_ACKED,          /* 차량이 접수 확인 */
    RREQ_UNKNOWN,        /* 전송했으나 응답 없음 — 결과 불명 */
    RREQ_DONE,           /* 최종 결과 수신 */
} rreq_ui_t;

RREQ_UNKNOWN을 생략하고 SENDING → 타임아웃 → 실패로 처리하는 게 흔한 실수다. 그러면 사용자는 잠기지 않았다고 믿고 다시 시도하는데, 실제로는 이미 잠겨 있다.


3. §6.5.3 — 차량 상태 및 오류 정보의 공통 표시 원칙

이 소절이 §6.5에서 가장 중요하다. 모든 기능의 표시가 따라야 할 공통 규칙이기 때문이다.

  • 차량 내 HMI 및 모바일 인터페이스는 각 기능이 제공하는 상태 정보, 요청 처리 결과 및 오류 상태를 사용자에게 제공해야 한다.
  • 차량 내 HMI 및 모바일 인터페이스는 차량 상태 정보가 최신이 아닌 경우 해당 정보가 최신 상태가 아님을 사용자에게 표시해야 한다.
  • 차량 내 HMI 및 모바일 인터페이스는 특정 기능의 상태 정보를 신뢰할 수 없는 경우 마지막 정상 값을 현재 정상 상태로 표시하지 않아야 한다.
  • 차량 내 HMI 및 모바일 인터페이스는 센서 오류, 통신 오류 및 기능 자체 오류를 사용자가 구분할 수 있도록 표시해야 한다.
  • 차량 내 HMI 및 모바일 인터페이스는 안전 보호 동작 또는 위험 경고를 일반 상태 정보보다 우선하여 사용자가 식별할 수 있도록 제공해야 한다.

3-1. 두 번째와 세 번째 문장은 같은 문제의 두 측면이다

최신이 아닌 경우  →  최신이 아님을 표시해야 한다        (능동적 표시)
신뢰할 수 없는 경우 →  정상 상태로 표시하지 않아야 한다   (금지)

금지만으로는 부족하다. 값을 안 보여주면 화면이 비고, 사용자는 "왜 아무것도 없지?"라고 생각한다. "이 정보는 오래됐다"를 적극적으로 알려야 한다.

UI 패턴으로는:

방식장단점
회색 처리 + 아이콘값은 보이되 흐리게, 시계 아이콘마지막 값을 참고할 수 있음. 오해 여지도 있음
타임스탬프 병기"23 °C (2분 전)"명확. 공간 차지
값 대체"— —" 또는 "확인 중"오해 없음. 정보 손실
애니메이션로딩 스피너일시적 상황에 적합

SR은 방식을 지정하지 않는다. "최신 상태가 아님을 표시"라는 관찰 가능한 결과만 요구한다.

/* HMI 값 렌더링 — 신뢰도를 항상 함께 다룬다 */
typedef struct {
    int16_t   value;
    bool      valid;
    uint32_t  updated_ms;
} hmi_value_t;

static void render_temp(const hmi_value_t *v)
{
    uint32_t age = hal_now_ms() - v->updated_ms;

    if (!v->valid) {                            /* §6.5.3 — 정상 값으로 표시 금지 */
        ui_draw_text("— —");
        ui_draw_badge(BADGE_SENSOR_ERROR);      /* 오류 종류를 구분 (아래 3-2) */
        return;
    }
    if (age > STALE_THRESHOLD_MS) {             /* §6.5.3 — 최신이 아님을 표시 */
        ui_draw_text_dimmed(fmt_temp(v->value));
        ui_draw_badge(BADGE_STALE);
        ui_draw_text_small(fmt_age(age));       /* "2분 전" */
        return;
    }
    ui_draw_text(fmt_temp(v->value));
}

TBD-022가 STALE_THRESHOLD_MS를 확정한다: "센싱/Vision 데이터 최대 age — 센서 종류별 유효 시간 확정".

센서 종류별로 다르다는 게 중요하다.

데이터적절한 max age근거
후방 거리(초음파)수백 ms차량이 움직이므로 빠르게 낡는다
실내 온도수 초~수십 초온도는 천천히 변한다
탑승자 존재수 초사람이 타고 내리는 속도
도어 잠금 상태이벤트 기반 + heartbeat변화가 드물지만 중요하다

하나의 전역 타임아웃을 쓰면 반드시 어딘가가 틀린다. 초음파에 맞추면 온도가 계속 stale로 뜨고, 온도에 맞추면 초음파가 위험하게 낡는다.

3-2. 오류 3종 구분 — 00편에서 예고한 것

"차량 내 HMI 및 모바일 인터페이스는 센서 오류, 통신 오류 및 기능 자체 오류를 사용자가 구분할 수 있도록 표시해야 한다."

오류무엇이 고장났나사용자가 할 수 있는 일표시 예
센서 오류센서 소자·배선센서 가림 확인, 정비센서 아이콘 + X
통신 오류노드 간 링크재시동, 잠시 대기링크 아이콘 + 끊김
기능 자체 오류제어 로직·액추에이터정비기능 아이콘 + 경고

세 가지가 같은 화면에 동시에 나타날 수 있다. 예를 들어 습도 센서가 죽고(센서 오류) Power Window 노드와 통신이 끊기고(통신 오류) Fan 요구-실제가 불일치(기능 오류)하는 상황.

typedef enum {
    ERR_KIND_NONE = 0,
    ERR_KIND_SENSOR,      /* §5 "센서 오류" — 센서 정보를 정상적으로 사용할 수 없음 */
    ERR_KIND_COMM,        /* §5 "통신 오류" — 통신 정보를 신뢰할 수 없음 */
    ERR_KIND_FUNCTION,    /* 기능 자체 오류 */
} err_kind_t;

typedef struct {
    uint8_t     function_id;
    err_kind_t  kind;
    uint16_t    detail;        /* 기능별 세부 코드 */
} fault_entry_t;

/* 오류를 단일 플래그로 뭉치면 §6.5.3 을 만족할 수 없다 */
static fault_entry_t g_faults[MAX_FAULTS];

3-3. 안전 정보의 표시 우선순위

"차량 내 HMI 및 모바일 인터페이스는 안전 보호 동작 또는 위험 경고를 일반 상태 정보보다 우선하여 사용자가 식별할 수 있도록 제공해야 한다."

06편의 Ambient Lighting 표시 우선순위와 같은 원칙이 HMI에도 적용된다.

세 채널이 같은 우선순위 철학을 공유한다:

채널우선순위 규칙근거 절
소리(VSS)안티핀치 > 비상경고 > 일반 > 피드백§6.1.2
빛(Ambient)Emergency > Fault > Warning > 피드백 > Goodbye > 일반§6.4.3
화면(HMI)안전 보호·위험 경고 > 일반 상태 정보§6.5.3

이 일관성이 우연이 아니다. 세 채널이 서로 다른 우선순위를 쓰면, 소리는 경고를 내는데 화면은 평온한 상태를 보여주는 모순이 생긴다.

HMI 구현에서는 보통 모달 오버레이 또는 전용 알림 영역으로 처리한다.

/* HMI 렌더 순서 — 안전 정보가 항상 위에 그려진다 */
void hmi_render_frame(void)
{
    render_base_layer();              /* 일반 상태 정보 */
    render_function_cards();

    /* §6.5.3 — 안전 정보는 마지막에, 즉 가장 위에 */
    if (safety_alert_active()) {
        render_safety_overlay();      /* 다른 것을 가려도 된다 */
    }
}

4. §6.5.4 — 모바일 인터페이스 공통 기능

  • 모바일 인터페이스는 사용자가 현재 차량과의 연결 가능 상태를 확인할 수 있도록 해야 한다.
  • 모바일 인터페이스는 최근 원격 차량 제어 요청의 수행 결과를 사용자가 확인할 수 있도록 해야 한다.
  • 모바일 인터페이스는 원격 차량 제어 요청이 거부되거나 실패한 경우 해당 요청과 거부 또는 실패 사유를 사용자가 확인할 수 있도록 해야 한다.
  • 모바일 인터페이스는 안전 관련 경고 또는 중요 상태 변화를 일반 상태 정보와 구분하여 사용자에게 제공해야 한다.
  • 모바일 인터페이스는 사용자가 확인하지 않은 중요 경고가 존재하는 경우 해당 경고 상태를 확인할 수 있도록 해야 한다.

4-1. 모바일에만 있는 요구사항 두 가지

차량 내 HMI에는 없고 모바일에만 있는 것:

(1) 연결 가능 상태 표시

차 안의 HMI는 차와 물리적으로 연결돼 있으므로 "연결됐나?"를 물을 필요가 없다. 모바일은 다르다 — 사용자가 차에서 100 m 떨어져 있을 수도, 지하 주차장에 있을 수도 있다.

**"연결 가능 상태"**라는 표현이 정확하다. 지금 연결돼 있는지가 아니라, 연결할 수 있는지를 보여준다. 사용자가 "지금 원격 명령을 보낼 수 있는가"를 판단할 근거가 된다.

(2) 미확인 경고 관리

"모바일 인터페이스는 사용자가 확인하지 않은 중요 경고가 존재하는 경우 해당 경고 상태를 확인할 수 있도록 해야 한다."

차 안의 HMI는 사용자가 앞에 앉아 있으므로 경고를 보게 된다. 모바일은 사용자가 앱을 안 켜고 있을 수 있다.

그래서 모바일에는 읽음/안 읽음 상태가 필요하다.

경고 발생 (예: 엔진룸 동물 진입)
  → 푸시 알림 발송 (§6.3.5 "즉시 알려야 한다")
  → 사용자가 앱을 안 열었다면? → 미확인 상태로 보존
  → 앱을 열면 미확인 경고 목록을 보여준다

이건 §6.3.5의 "즉시 알려야 한다"(push)와 "조회할 수 있도록"(pull)의 조합을 보완한다. 푸시가 실패하거나 무시돼도 정보가 사라지지 않는다.

typedef struct {
    uint32_t   event_id;
    uint8_t    severity;       /* 중요 경고인가 */
    uint32_t   occurred_ms;
    bool       acknowledged;   /* §6.5.4 — 사용자 확인 여부 */
} mobile_alert_t;

/* 미확인 중요 경고 개수 — 앱 배지에 표시 */
static uint8_t unacked_critical_count(void)
{
    uint8_t n = 0;
    for (int i = 0; i < g_alert_count; i++)
        if (g_alerts[i].severity >= SEV_CRITICAL && !g_alerts[i].acknowledged) n++;
    return n;
}

4-2. "최근 요청의 수행 결과"

"모바일 인터페이스는 최근 원격 차량 제어 요청의 수행 결과를 사용자가 확인할 수 있도록 해야 한다."

이력(history)이 필요하다는 뜻이다. 사용자가 "아까 잠금 요청이 성공했나?"를 나중에 확인할 수 있어야 한다.

이건 §6.5.2의 "연결 종료 시 자동 재전송하지 않는다"와 짝을 이룬다. 재전송을 안 하는 대신, 사용자가 결과를 확인하고 스스로 다시 시도할 수 있게 해야 한다. 이력이 없으면 사용자는 판단할 근거가 없다.


5. §6.6 — 기능별 상태 및 사용자 알림

  • 시스템은 기능별 동작 상태를 서로 구분해야 한다.
  • 시스템은 센서 오류통신 오류를 서로 구분해야 한다.
  • 시스템은 개별 기능에 오류가 발생하더라도 독립적으로 안전하게 운용 가능한 다른 기능을 불필요하게 중단하지 않아야 한다.
  • HMI 는 기능별 동작 상태를 사용자에게 제공해야 한다.
  • HMI 는 센서 오류 상태를 사용자에게 제공해야 한다.
  • HMI 는 통신 오류 상태를 사용자에게 제공해야 한다.
  • HMI 는 신뢰할 수 없는 상태 정보를 현재 정상 상태로 표시하지 않아야 한다.

5-1. 이 절은 앞의 모든 것의 요약이다

§6.6의 일곱 문장은 §6.1~§6.5에서 이미 나온 원칙들을 시스템 전체 레벨로 다시 선언한 것이다.

§6.6 문장어디서 이미 나왔나
기능별 동작 상태 구분§6.1.4, §6.2.6, §6.4.1, §6.4.2, §6.4.3 각각의 상태 요구사항
센서 오류통신 오류 구분§6.3.1, §6.5.3
기능 오류 격리§6.3.2, §6.4.4
신뢰할 수 없는 정보를 정상으로 표시 금지§6.2.6, §6.3.4, §6.5.3

왜 반복하는가? 같은 원칙이 특정 기능의 맥락에서만 성립하는 게 아니라 시스템 전역 원칙임을 못 박기 위해서다. 새로운 기능이 추가되어도 이 규칙은 적용된다.

00편 패턴 B(freedom from interference)가 세 번째로 등장한 것도 여기다. §6.3.2(센서), §6.4.4(Smart Access/Climate/Ambient), §6.6(전 기능) — 범위를 넓혀 가며 세 번 반복된다.

5-2. 기능별 상태를 어떻게 구조화하는가

"기능별 동작 상태를 서로 구분해야 한다"를 만족하는 자료구조:

typedef enum {
    FUNC_VSS = 0,
    FUNC_POWER_WINDOW,
    FUNC_SENSING,
    FUNC_VISION,
    FUNC_SMART_ACCESS,
    FUNC_PREDICTIVE_CLIMATE,
    FUNC_AMBIENT_LIGHTING,
    FUNC_COUNT
} function_id_t;

typedef enum {
    FSTATE_INIT,          /* 초기화 중 */
    FSTATE_READY,         /* 정상 — 요청 처리 가능 */
    FSTATE_ACTIVE,        /* 동작 중 */
    FSTATE_DEGRADED,      /* 일부 제한 — 입력 불신 등 */
    FSTATE_FAULT,         /* 고장 — 기능 사용 불가 */
    FSTATE_DISABLED,      /* 사용자가 비활성화 */
} function_state_t;

typedef struct {
    function_state_t  state;
    err_kind_t        err_kind;      /* §6.6 — 센서/통신/기능 구분 */
    uint16_t          err_detail;
    uint32_t          updated_ms;    /* §6.5.3 — age 판정용 */
} function_status_t;

/* 기능마다 독립된 상태 — 전역 system_ok 플래그는 금지 (§6.6) */
static function_status_t g_func[FUNC_COUNT];

g_func 배열이 §6.6의 세 요구사항을 한꺼번에 만족시킨다.

  • 기능별 구분 → 배열 인덱스
  • 오류 종류 구분 → err_kind 필드
  • 기능 격리 → 한 원소의 FAULT가 다른 원소에 영향 없음

반대로 이런 구현은 §6.6을 구조적으로 위반한다:

/* ✗ 전역 단일 상태 — 격리 불가능 */
static bool g_system_ok;
static uint32_t g_error_flags;   /* 비트 플래그 — 종류 구분 없음 */

5-3. DEGRADED가 왜 필요한가

01편의 VSS 상태 기계에서 READY / DEGRADED / FAULT를 나눴던 것과 같은 이유다.

FAULT    : 기능 자체가 고장. 아무것도 못 한다
DEGRADED : 기능은 살아 있으나 입력이 부족해 일부 동작만 가능

Predictive Climate를 예로 들면:

상황상태가능한 것
정상ACTIVE자동 공조, Pre-conditioning, 자동 환기 전부
습도 센서 고장DEGRADED온도 기반 자동 공조 ○, 습도 기반 환기 판단 ✗
Fan 요구-실제 불일치FAULT아무 Fan 제어도 못 함

DEGRADED가 없으면 습도 센서 하나 때문에 공조 전체가 죽는다. 그건 §6.6의 세 번째 요구사항("독립적으로 안전하게 운용 가능한 다른 기능을 불필요하게 중단하지 않아야 한다") 위반이다.


6. HMI가 상태를 받는 경로 — TBD-001과 TBD-021

6-1. ESP32와 Central 사이가 미정이다

TBD-001: "Central ControllerESP32 물리 통신 — UART/CAN/Wi-Fi 등 팀 공통 방식 확정"

세 후보의 성격이 완전히 다르다.

방식대역폭지연배선특징
UART낮음(수백 kbps)낮음2선단순. 프레이밍·오류 검출을 직접 구현
CAN중간(1 Mbps, FD면 더)낮음, 결정적2선(차동)노이즈 강함. 아비트레이션·CRC 내장. 차량 표준
Wi-Fi높음높고 변동무선대역폭 크지만 실시간성 나쁨. 보안 부담

이 시스템의 성격상 CAN이 유력하다. 다른 세 노드가 이미 CAN을 쓰고 있어 프로토콜을 통일할 수 있고, HMI가 받아야 할 상태 정보가 다른 노드의 CAN 프레임과 같은 성격이기 때문이다.

다만 ESP32의 CAN 컨트롤러(TWAI) 지원 범위를 먼저 확인해야 한다. 오래 쓰인 ESP32/S3/C3 계열의 TWAI는 Classic CAN 전용이고, CAN FD는 최근 변종에서만 지원된다. TBD-002가 "CAN FD 파라미터"를 확정 항목으로 두고 있으므로, 채택할 ESP32 변종이 FD를 지원하지 않으면 TBD-001은 CAN이 될 수 없다.

즉 TBD-001과 TBD-002는 하드웨어 제약으로 얽혀 있어 함께 결정해야 한다. §8의 표에서는 두 항목이 독립돼 보인다.

다이어그램 로딩 중…

이런 TBD 간 의존을 문서에서 찾아내는 게 SR 리뷰의 실질적 가치다. §8의 표만 봐서는 두 항목이 독립돼 보인다.

6-2. TBD-021 — HMI 상태 갱신 재전송

TBD-021: "HMI 상태 갱신 재전송 — 통합 시험 후 확정"

문제는 이렇다. 상태가 변할 때만 보내면(이벤트 방식) 그 프레임이 유실됐을 때 HMI가 영원히 옛 상태를 표시한다. 그런데 §6.5.3은 stale 표시를 금지한다.

해법은 이벤트 + 주기 갱신의 조합이다.

/* 상태 송신 정책 */
#define STATE_PERIODIC_MS   500u    /* TBD-021 — 주기 재전송 */

static void state_tx_tick(function_id_t f)
{
    bool changed = state_changed_since_last_tx(f);
    bool due     = (hal_now_ms() - g_last_tx_ms[f]) >= STATE_PERIODIC_MS;

    if (changed || due) {
        can_send_state(f, &g_func[f]);
        g_last_tx_ms[f] = hal_now_ms();
    }
}

이벤트로 반응성을, 주기로 신뢰성을 얻는다. 그리고 주기 송신이 있으면 수신 측이 age를 계산할 수 있어 §6.5.3의 stale 판정이 가능해진다. TBD-020(Heartbeat/Timeout)과 TBD-021, TBD-022가 한 묶음인 이유다.

TBD-020 heartbeat 주기 ─┐
TBD-021 상태 재전송 주기 ─┼─→ 수신 측 max age(TBD-022)를 정하는 근거
                        ─┘   (재전송 주기 × N + 여유)

max age는 재전송 주기보다 반드시 커야 한다. 500 ms마다 보내는데 max age가 300 ms면 항상 stale로 판정된다. 보통 max_age = 재전송 주기 × 2~3 + 전송 지연으로 잡는다.


6-bis. 양산차는 어떻게 하나 — 이 프로토타입과의 거리

현대 블루링크·BMW ConnectedDrive·테슬라 앱은 전부 클라우드를 거친다.

폰 앱 ──인터넷──▶ OEM 클라우드 ──셀룰러──▶ 차량 TCU ──CAN──▶ 각 ECU
                      ↑
                  인증·권한·큐잉·감사로그

앱이 차에 직접 붙지 않는 이유가 넷이다.

이유설명
차는 움직인다IP 가 고정되지 않고 NAT·방화벽 뒤에 있다
차는 대부분 잔다클라우드가 명령을 큐에 담아 뒀다가 차가 깨어날 때 전달
인증·권한차량 공유·소유권 이전·가족 계정을 중앙 관리
감사 로그누가 언제 원격으로 열었는지

그런데 근거리는 직접 붙는다

클라우드만 쓰면 지하주차장에서 문이 안 열린다. 그래서 실제로는 두 경로를 병행한다.

근거리   폰 ←── BLE / UWB / NFC ──→ 차량      Digital Key. 통신이 죽어도 동작해야 한다
원거리   폰 ←인터넷→ 클라우드 ←셀룰러→ TCU     원격 제어·상태 조회

여기 표준이 있다 — CCC Digital Key(Car Connectivity Consortium). Apple CarKey·삼성·구글이 전부 이걸 쓴다.

Release 1~2   NFC          폰을 도어 핸들에 대야 한다
Release 3     BLE + UWB    주머니에 넣고 다가가면 열린다 (패시브 엔트리)
Release 4     확장         차량 공유·원격 위임

Release 3 에서 UWB 가 들어온 이유가 릴레이 공격이다. BLERSSI 는 중계기로 위조할 수 있지만, UWB 는 신호의 비행 시간을 재므로 거리를 줄일 수 없다.

이 프로토타입의 위치

§7.1 이 범위를 잘랐다 — "프로토타입의 로컬 네트워크/BLE 환경에서 검증한다."

Android 앱 ←── BLE ──→ ESP32      Digital Key 인증, 근접 판정(RSSI)
Android 앱 ←── Wi-Fi ──→ ESP32     제어 요청, 상태 스트림, 알림
                          ↓ TBD-001
                    Central Controller

클라우드도 셀룰러도 없다. 그래서 §6.3.5 의 "즉시 알려야 한다"를 FCM 푸시로 구현할 수 없고, 앱이 직접 연결을 유지해야 한다. 이게 이 앱에서 가장 까다로운 구현이고, 다음 절이 그 방법을 다룬다.

프로토타입에 없는 것실제로는
클라우드 중계OEM 클라우드 + TCU
셀룰러LTE/5G 상시 연결
FCM 푸시클라우드가 푸시 발송
UWB 거리 측정CCC Release 3
명령 큐잉클라우드가 보관 후 전달

알려진 한계로 문서화해야 할 항목들이다. 특히 UWB 가 없으므로 릴레이 공격에 원리적으로 취약하다.


6-ter. Android 구현 — 최근 제약이 많이 바뀌었다

[2026-09 기준] Android 의 백그라운드·BLE 정책이 최근 몇 년간 계속 조여졌다. 예전 블로그 코드를 그대로 쓰면 동작하지 않는다.

지금 지켜야 하는 것들

targetSdk 36 (Android 16)   2026-08-31 부터 Play 신규·업데이트 요건
BLE 권한 3종                Android 12(API 31) 부터 런타임 권한
Foreground Service 타입      Android 14(API 34) 부터 필수 선언
connectedDevice 강제         Android 15 부터 백그라운드 BLE 는 이 타입이어야 한다
백그라운드 FGS 시작 차단      Android 15 부터. 부팅·복귀 시 재연결에 영향

프로젝트라 Play 배포는 안 하더라도 targetSdk 는 올려야 한다. 동작 변경은 targetSdk 기준으로 적용되므로, 낮게 두면 나중에 올릴 때 한꺼번에 깨진다.

BLE 권한 — Android 12 이후

<!-- 구버전 호환 -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />

<!-- Android 12+ 런타임 권한 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
                 android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

<!-- 위치를 쓰지 않으므로 30 까지만 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
                 android:maxSdkVersion="30" />
권한언제 필요
BLUETOOTH_SCAN기기를 찾을 때
BLUETOOTH_CONNECT이미 페어링된 기기와 통신할 때
BLUETOOTH_ADVERTISE내가 발견되게 할 때 (이 앱은 불필요)

neverForLocation 플래그가 중요하다. 이걸 붙이면 ACCESS_FINE_LOCATION 없이도 스캔할 수 있다 — 차량 앱은 BLE 로 위치를 추정하지 않으므로 붙이는 게 맞다. 사용자에게 위치 권한을 안 물어도 되니 설치 이탈이 준다.

대가가 하나 있다 — neverForLocation 을 붙이면 시스템이 일부 비콘을 스캔 결과에서 걸러낸다. 우리 차량은 자체 서비스 UUID 로 광고하므로 문제없다.

Foreground Service — connectedDevice 타입

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />

<service
    android:name=".VehicleLinkService"
    android:foregroundServiceType="connectedDevice"
    android:exported="false" />
ServiceCompat.startForeground(
    this, NOTIFICATION_ID, buildOngoingNotification(),
    ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
)

Android 14 부터 타입을 선언하지 않으면 MissingForegroundServiceTypeException 으로 죽는다. 그리고 connectedDevice 타입을 쓰려면 런타임에 BLUETOOTH_CONNECT·BLUETOOTH_SCAN·BLUETOOTH_ADVERTISE·UWB_RANGING하나 이상이 실제로 부여돼 있어야 한다.

그런데 — 이걸 직접 하지 않는 게 낫다

포그라운드 서비스로 연결을 유지하려 하면 싸울 것이 계속 나온다.

Doze 모드                     화면이 꺼지면 네트워크·알람이 묶인다
배터리 최적화 화이트리스트       사용자에게 예외 등록을 요청해야 한다
Android 15 백그라운드 FGS 차단  부팅 후·강제종료 후 스스로 다시 못 뜬다
상시 알림                     사용자가 계속 보게 된다

Android 가 이 문제를 위해 만든 전용 API 가 있다.


6-quater. Companion Device Manager — 이 앱의 정답

CDM 은 "내 앱이 특정 기기 하나와 계속 붙어 있어야 한다"를 위해 만들어졌다. 스마트워치·피트니스 밴드가 주 대상인데, 차량 앱이 정확히 같은 형태다.

무엇을 주나

얻는 것설명
위치 권한 불필요시스템이 대신 스캔한다. ACCESS_FINE_LOCATION 이 필요 없다
근접 이벤트기기가 범위에 들어오고 나가는 것을 시스템이 알려준다
자동 서비스 바인딩기기가 가까우면 서비스가 뜨고, 멀어지면 내려간다
백그라운드 제약 면제아래 셋이 페어링 성공 시 자동 부여된다
REQUEST_COMPANION_RUN_IN_BACKGROUND                          API 26+
REQUEST_COMPANION_USE_DATA_IN_BACKGROUND                     API 26+
REQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND  API 31+

마지막 것이 결정적이다. Android 15 가 막은 "백그라운드에서 포그라운드 서비스 시작"을 이 앱만 예외로 허용받는다. 부팅 후 차량이 가까워지면 앱이 스스로 뜬다.

이 권한들은 매니페스트에도 런타임 요청에도 쓰지 않는다. 페어링이 성공하면 시스템이 알아서 준다. 앱을 지우거나 disassociate() 를 부르면 회수된다.

페어링 흐름

<uses-feature android:name="android.software.companion_device_setup" />
val filter = BluetoothLeDeviceFilter.Builder()
    .setNamePattern(Pattern.compile("VCS-.*"))          // 차량 광고 이름
    .build()

val request = AssociationRequest.Builder()
    .addDeviceFilter(filter)
    .setSingleDevice(true)                               // 차 한 대만 고른다
    .build()

val cdm = getSystemService(CompanionDeviceManager::class.java)

cdm.associate(request, mainExecutor, object : CompanionDeviceManager.Callback() {
    override fun onAssociationPending(intentSender: IntentSender) {
        // 시스템이 기기 선택 UI 를 띄운다 — 우리가 목록을 그리지 않는다
        startIntentSenderForResult(intentSender, REQ_SELECT_DEVICE, null, 0, 0, 0)
    }

    override fun onAssociationCreated(info: AssociationInfo) {
        prefs.saveAssociationId(info.id)                  // 이 id 로 이후를 관리한다
    }

    override fun onFailure(error: CharSequence?) { /* 사용자에게 사유 표시 */ }
})

기기 선택 UI 를 시스템이 그려 준다. 사용자가 목록에서 자기 차를 고르고, 그 순간 위 권한들이 부여된다.

근접 감시 — Android 16 에서 API 가 바뀌었다

// Android 16(API 36) 이후
cdm.startObservingDevicePresence(
    ObservingDevicePresenceRequest.Builder(associationId).build()
)
class VehiclePresenceService : CompanionDeviceService() {
    override fun onDeviceEvent(event: DevicePresenceEvent) {
        // 차량이 범위에 들어오거나 나갈 때 시스템이 바인딩/언바인딩한다
    }
}

구 API 는 API 36 에서 deprecated 됐다startObservingDevicePresence(String), onDeviceAppeared(), onDeviceDisappeared(). 구버전 호환이 필요하면 두 경로를 다 두어야 한다.

서비스 수명을 시스템이 관리한다. 차가 가까우면 바인딩되고 멀어지면 내려가므로, 우리가 상시 포그라운드 서비스를 붙들고 있을 필요가 없다.

그래도 남는 제약

위치 서비스 자체는 켜져 있어야 한다     (권한은 불필요, 기능은 필요)
로그아웃·차량 해제 시 disassociate()   안 하면 연결이 남는다

6-quinquies. GATT 설계

BLE 로 데이터를 주고받는 규칙이 GATT 다. BLE 에는 "메시지를 보낸다"는 개념이 없고, 서로 합의한 변수 목록을 읽고 쓰는 형태만 있다. 그 변수 하나가 캐릭터리스틱이고, 관련된 것들을 묶은 게 서비스다.

Service: Vehicle Control (128-bit custom UUID)
  Auth        Write + Notify   챌린지-응답 (Digital Key)
  Request     Write            제어 요청 (req_id 포함)
  ReqResult   Notify           요청 결과 6단계
  State       Read + Notify    상태 스냅샷
  Alert       Notify           중요 경고 (ROA · 엔진룸 동물)

핵심 설계 결정 — ESP32CAN 에서 받은 상태를 재포장하지 않는다. 10편의 신호 구조를 그대로 앱까지 흘려야 *_trust 필드가 보존된다. 중간에서 "예쁘게" JSON 으로 바꾸면서 유효성을 떨어뜨리면 §6.5.3 을 만족할 방법이 사라진다.

// 값과 신뢰도를 절대 분리하지 않는다
sealed interface Trusted<out T> {
    data class Ok<T>(val value: T, val updatedAt: Long) : Trusted<T>
    data class Stale<T>(val last: T, val updatedAt: Long) : Trusted<T>
    data class Invalid(val kind: ErrKind, val detail: Int) : Trusted<Nothing>
    data object NoData : Trusted<Nothing>
}

enum class ErrKind { SENSOR, COMM, FUNCTION }        // §6.5.3 — 3종 구분 필수

Trusted<Int> 로 감싸면 UI 에서 신뢰도를 빼먹을 수가 없다. when 이 모든 분기를 강제한다. Int 로 들고 있으면 반드시 어딘가에서 stale 을 정상처럼 그리게 된다.

MTU 와 페이로드

BLE 기본 MTU 는 23 바이트(유효 20 바이트)다. 상태 스냅샷이 그보다 크면 나눠 보내야 한다.

gatt.requestMtu(247)      // 협상. 기기·OS 에 따라 더 작게 잡힐 수 있다

협상 결과를 반드시 확인하고 그 크기에 맞춰 잘라야 한다. 요청한 값이 그대로 오는 게 아니다.

알림(Notify) 을 켜는 것을 잊지 않는다

gatt.setCharacteristicNotification(characteristic, true)          // ① 로컬 설정
val cccd = characteristic.getDescriptor(CCCD_UUID)                // ② 원격에도 알린다
cccd.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(cccd)

①만 하고 ②를 빠뜨리면 알림이 안 온다. BLE 입문자가 가장 많이 걸리는 함정이다.


7. 한 줄 요약

한 줄
§6.5.1HMI는 요청을 생성할 뿐, 실행은 기능의 허용 조건이 결정한다. 낙관적 UI 금지
§6.5.2인증 + 차량별 권한 둘 다 필요. 연결 종료 시 자동 재전송 금지. "결과 불명" 상태를 표시해야 함
§6.5.3최신이 아니면 그렇다고 표시. 센서/통신/기능 오류 3종 구분. 안전 정보가 일반 정보보다 우선
§6.5.4모바일에만: 연결 가능 상태, 요청 이력, 미확인 경고 관리
§6.6기능별 상태를 독립 관리. 전역 system_ok 플래그는 구조적 위반
HMI 제거SR 의 표시 요구사항이 전부 모바일로 이관. §6.3.3 후방 경고만은 그대로 못 옮긴다
양산 대비실제는 클라우드 경유 + 근거리 BLE 이중. 이 프로토타입은 로컬 전용
AndroidtargetSdk 36 · BLE 권한 3종 · FGS connectedDevice · CDM 이 정답
CDM위치 권한 불필요 + 근접 이벤트 + 백그라운드 제약 면제 자동 부여

설계 판단 체크포인트

구현에 들어가기 전에 팀이 답을 갖고 있어야 하는 것들이다. 답이 안 나오면 그 자리가 곧 설계 문의 또는 TBD 후보다.

  1. HMI에서 잠금 버튼을 누르는 즉시 아이콘을 "잠김"으로 바꾸는 구현의 문제는? §6.5.1이 "실제 수행 여부는 해당 기능의 허용 조건 및 안전 정책에 따라 결정"된다고 하고, §6.4.1은 열린 문에 잠금을 수행하지 않는다. 요청이 거부될 수 있으므로 낙관적 표시는 거짓 정보가 된다. 요청 상태("요청 보냄")와 도어 상태를 분리해 표시해야 한다.

  2. 원격 요청 후 연결이 끊겼다. "실패"로 표시하면 무엇이 문제인가? 요청이 도달해 실행됐을 수도 있다. "실패"라고 하면 사용자가 다시 시도해 중복 동작을 일으킨다. §6.5.4가 "접수 여부"와 "최종 처리 결과"를 별도로 요구하는 이유다. "전송했으나 결과 확인 불가" 상태가 필요하다.

  3. max age를 모든 센서에 동일하게 300 ms로 잡으면? 실내 온도는 그렇게 자주 갱신될 필요가 없어 항상 stale로 표시되고, 사용자는 온도를 못 본다. 반대로 초음파 거리에는 300 ms도 길 수 있다(차량 이동 중). TBD-022가 "센서 종류별 유효 시간"이라고 명시한 이유다. 데이터가 낡는 속도는 물리량마다 다르다.

  4. 전역 bool g_system_ok 플래그로 오류를 관리하면 §6.6의 어느 요구사항을 위반하나? 세 개를 동시에 위반한다. (1) "기능별 동작 상태를 서로 구분해야 한다" — 구분이 없다. (2) "센서 오류통신 오류를 서로 구분해야 한다" — 종류가 없다. (3) "개별 기능 오류가 다른 기능을 불필요하게 중단하지 않아야 한다" — 하나가 false면 전부 죽는다.

  5. TBD-001(Central↔ESP32 통신)과 TBD-002(CAN FD 파라미터)가 왜 함께 결정돼야 하나? 흔히 쓰이는 ESP32 계열의 CAN 컨트롤러(TWAI)는 Classic CAN 전용이다. TBD-002에서 CAN FD를 채택했는데 채택한 ESP32 변종이 FD를 지원하지 않으면 ESP32를 CAN 버스에 붙일 수 없고, TBD-001은 UART나 Wi-Fi가 되어야 한다. §8의 TBD 표에서는 두 항목이 독립돼 보이지만 하드웨어 제약으로 얽혀 있다. 이런 의존을 찾는 게 SR 리뷰의 실질적 가치다.

  6. 상태를 이벤트 기반으로만 송신하면 §6.5.3의 어느 요구사항을 만족할 수 없나? "차량 상태 정보가 최신이 아닌 경우 해당 정보가 최신 상태가 아님을 표시해야 한다"를 만족할 수 없다. 이벤트 방식에서는 "변화가 없어서 안 온 것"과 "송신 노드가 죽어서 안 온 것"을 구분할 수 없기 때문이다. 주기 재전송(TBD-021) 또는 heartbeat(TBD-020)가 있어야 수신 측이 age를 계산해 stale을 판정할 수 있다.

Ambient Lighting — 표시 우선순위·식별 가능성·조도 조절요구사항 관리 — SR 작성 원칙·TBD 32건·리뷰 결과