BLE 동영상 송수신

shin_stealer·2026년 6월 29일

R10 JPEG BLE 스냅샷 — 학습 가이드

이 문서는 InstallerApp에서 블루투스(BLE)로 카메라 JPEG 사진을 받는 기능을 공부하기 위한 정리입니다.
중학생도 따라올 수 있도록 쉬운 말로 설명하고, 마지막에 실제 구현된 소스 파일전체 흐름을 연결합니다.

참고 원문: documents/BT JPEG 고속 수신 모바일 앱 적용 가이드 _ R10WS Docs.html


1. 먼저 알아두면 좋은 단어

블루투스(BLE)란?

스마트폰과 R10 장치가 가까운 거리에서 무선으로 데이터를 주고받는 통로입니다.
Wi‑Fi처럼 인터넷 전체가 아니라, 내 폰 ↔ 내 장치 사이 연결에 가깝습니다.

GATT란?

BLE 위에서 “어떤 데이터를 어디로 보낼지” 정해 놓은 약속(규칙) 입니다.

비유:

BLE/GATT비유
GATT 서비스아파트 한 동
Characteristic(특성)각 호실
UUID호실 번호판 (주소)

우리 앱은 호실 번호(UUID) 를 보고 “여기는 AT 명령용”, “여기는 JPEG 바이너리용”이라고 구분합니다.

TX / RX / TX-only

이름우리 앱에서
TX장치 → 앱 (보내기)notify로 데이터 수신
RX앱 → 장치 (받기)write로 AT 명령 전송
TX/RX양방향AT 명령 + 텍스트 응답
TX-only장치 → 앱만JPEG 바이너리만 연속 수신

TX-only = “JPEG 전용 수신 호실. 앱은 write 안 하고 받기만 한다.”

AT 명령이란?

앱이 장치에게 보내는 짧은 텍스트 명령입니다.

예:

AT$$JPG=CAPTURE,0,10000

→ “0번 카메라 JPEG 찍어줘. 10초 안에.”

장치도 텍스트로 대답합니다.

$$JPG: 0,O00000001,0,960,540,135564,... OK

→ “찍었어. sessionId는 O00000001, 크기는 135564바이트야.”

sessionId란?

한 번 찍은 JPEG 묶음의 주문 번호입니다.

  • 캡처 성공 → sessionId 발급
  • 그 sessionId로 JPEG 데이터 받기
  • 끝나거나 취소하면 → DROP으로 장치 메모리 정리

notify란?

장치가 먼저 데이터를 밀어 넣는 방식입니다.
앱이 “다음 거 줘”라고 매번 묻지 않아도 됩니다. 택배가 문 앞에 쌓이는 느낌.


2. JPEG 받는 방법 두 가지

예전 방식: GET (느리지만 단순)

  1. CAPTURE → 사진 정보(메타데이터) 받기
  2. AT$$XFER=GET,sessionId,0 → 0번 조각 요청
  3. AT$$XFER=GET,sessionId,1 → 1번 조각 요청
  4. … 반복 …
  5. EOF → 끝

조각마다 Base64 텍스트로 와서, 앱이 decode 해야 합니다.
AT 왕복이 많아서 사진이 클수록 느립니다.

새 방식: STREAM (빠름)

  1. CAPTURE → 메타데이터 (GET과 동일)
  2. AT$$XFER=STREAM,sessionId,0,0 → “이제 보내기 시작해”
  3. TX-only notify로 JPEG raw 바이트가 연속 도착
  4. EOF frame → 끝

데이터 구간은 장치 → 앱 단방향 push.
앱은 STREAM 시작 명령 한 번만 보냅니다.

비유: 책 배달

방식비유
GET“1페이지 보내줘” → 받음 → “2페이지 보내줘” → …
STREAM“책 통째로 보내 시작해” → 택배가 페이지를 계속 투입

3. 우리 앱이 쓰는 BLE 호실(UUID)

구분UUID역할
TX/RXFEC26EC4-6D71-4442-9F81-55BC21D658D6AT 명령 write, 텍스트 notify
TX-onlyB273BFED-974F-43A7-9F77-D28E9F7C2D41JPEG binary stream notify

연결할 때 두 notify 모두 켜야 STREAM이 동작합니다.
(CCCD descriptor write까지 해야 실제 notify가 켜지는 폰도 많습니다.)


4. STREAM binary frame — “택배 상자 규격”

TX-only로 오는 데이터는 AX로 시작하는 작은 상자(frame) 들의 연속입니다.

[헤더 18바이트][JPEG 조각 N바이트]

헤더 안에 들어 있는 것 (쉽게):

필드의미
magic AX“올바른 상자야” 표시
frameType 1데이터 조각
frameType 2EOF (끝!)
streamIdSTREAM 시작할 때 받은 ID와 같은지 확인
seq0, 1, 2, … 순서대로 와야 함
chunkCrc32이 조각이 깨지지 않았는지 검사
payloadJPEG raw bytes

다 받으면 전체 CRC32 + SHA256으로 “사진 전체가 맞는지” 한 번 더 검사합니다.


5. DROP은 왜 자주 보였나?

AT$$JPG=DROP,sessionId = “이 주문(session) 그만. 메모리 비워.”

다음 상황에서 호출됩니다.

  • 다른 채널로 바꿀 때 (이전 session 정리)
  • 수신 실패 후 재시도 전
  • 화면 나갈 때
  • 타임아웃 / 사용자 취소

DROP 로그 자체는 버그가 아니라 정리 작업입니다.
다만 STREAM 직후 바로 DROP → CAPTURE가 반복되면 1차 수신 실패 후 retry 신호였고, 그건 race/CCCD 문제로 수정했습니다.


6. 앱에서 JPEG 한 장 받는 큰 그림

sequenceDiagram
    participant UI as R10Snapshot 화면
    participant VM as R10SnapshotViewModel
    participant Repo as BleRepository
    participant Gatt as BleGattConnection
    participant Dev as R10 장치

    UI->>VM: 채널 선택
    VM->>Repo: getSnapshot(channel)
    Repo->>Gatt: AT$$JPG=CAPTURE
    Gatt->>Dev: write TX/RX
    Dev-->>Gatt: $$JPG metadata notify
    Gatt-->>Repo: sessionId, totalBytes, crc, sha256
    Repo-->>VM: SnapshotCaptureInfo

    VM->>Repo: receiveTransfer(expectation)
    Note over Repo: STREAM 우선, 실패 시 GET fallback

    Repo->>Gatt: TX-only collector 시작
    Repo->>Gatt: AT$$XFER=STREAM
    Dev-->>Gatt: $$XFER STREAMING
    loop binary frames
        Dev-->>Gatt: TX-only notify
        Gatt-->>Repo: ByteArray chunks
    end
    Repo-->>VM: JPEG ByteArray
    VM-->>UI: 화면에 표시

7. 구현 소스 — 역할 정리

7-1. 화면 / ViewModel (app 모듈)

파일역할
R10SnapshotActivity.ktUI. 첫 진입 시 채널 선택 → ViewModel 호출. JPEG Bitmap 표시.
R10SnapshotViewModel.kt오케스트레이션. getSnapshotreceiveTransfer 순서 호출. retry, timeout, DROP, 채널 전환 mutex.
SnapshotBleErrorMapper.ktBLE 에러 → 사용자에게 보여줄 한글 메시지
SnapshotRetryPolicy.kt어떤 에러는 재시도할지 규칙

ViewModel 핵심 코드 (개념만):

val snapshotResult = bleRepository.getSnapshot(channel = channelId)
// 성공 시 ↓
val transferResult = bleRepository.receiveTransfer(info.toTransferExpectation())

→ ViewModel은 STREAM인지 GET인지 몰라도 됨. Repository가 알아서 선택.


7-2. 공개 API (blemanager)

파일역할
BleRepository.kt앱이 쓰는 인터페이스. getSnapshot, receiveTransfer, dropSnapshot 선언.
BleRepositoryImpl.kt실제 구현. receiveTransfer에서 STREAM → GET fallback 분기.

7-3. BLE 연결 / notify (blemanager)

파일역할
BleDeviceProfile.kt제품별 UUID 정의 인터페이스
R9BleProfile.ktTX/RX, TX-only UUID 실제 값
BleGattConnection.ktGATT 연결, AT write/read, notify 수신. TX/RX → UTF-8 텍스트, TX-only → txOnlyBinaryStream

BleGattConnection이 하는 일

  1. 연결 후 TX/RX + TX-only notify + CCCD enable
  2. sendAtCommand() — AT 명령 보내고 텍스트 응답 대기
  3. TX-only onCharacteristicChanged — byte[] 그대로 SharedFlow emit

7-4. JPEG 캡처 (CAPTURE) — snapshot 패키지

파일역할
SnapshotCaptureInfo.ktCAPTURE 성공 결과 (sessionId, 크기, crc, sha256 등)
BleJpgResponseParser.kt$$JPG: 응답 문자열 파싱
BleSnapshotError.kt캡처 단계 에러 종류
BleSnapshotException.kt캡처 예외 클래스

SnapshotCaptureInfo.toTransferExpectation() — 수신 단계로 넘길 검증 기준 만들기.


7-5. JPEG 수신 (XFER) — transfer 패키지

STREAM 경로 (새로 추가된 핵심)

파일역할
BleXferStreamResponseParser.kt$$XFER: ... STREAMING ... 응답 파싱, streamId 추출
BleBinaryStreamFrameParser.ktTX-only AX frame 파싱, seq/CRC 검사, notify 분할 reassembly
BleTransferStreamReceiver.ktframe 모아 JPEG buffer 조립 → EOF 후 검증

GET fallback 경로 (예전 방식, 유지)

파일역할
BleTransferReceiver.ktAT$$XFER=GET 반복, Base64 decode, 조립
BleXferResponseParser.ktGET 응답 파싱

공통

파일역할
TransferExpectation.ktsessionId, totalBytes, crc32, sha256 (받을 때 목표치)
TransferChecksumVerifier.ktCRC32 / SHA256 계산·비교
TransferBufferVerifier.kt다 받은 뒤 전체 크기·checksum 최종 검증
BleTransferError.kt수신 에러 종류 (STREAM_START_FAILED 포함)
BleTransferException.kt수신 예외 (fallbackEligible — GET fallback 가능 여부)

BleTransferSender.kt 등은 인증서/CAN DB 업로드용 XFER PUT. JPEG 다운로드와는 다른 방향.


8. receiveTransfer 내부 흐름 (STREAM 우선)

BleRepositoryImpl.receiveTransfer() 요약:

1. TX-only notify 사용 가능?
   └─ No  → GET만 사용
   └─ Yes → STREAM 시도
              ├─ 성공 → JPEG 반환 (Log: transferMode=STREAM)
              └─ 시작 실패 & fallbackEligible
                    → GET fallback (Log: transferMode=GET_FALLBACK)
              └─ 수신 중 CRC/seq 실패
                    → fallback 없이 failure (재촬영/retry)

STREAM 상세 (receiveTransferViaStream)

1. Channel 열고 txOnlyBinaryStream collector 먼저 시작  ← race 방지
2. AT$$XFER=STREAM,sessionId,0,0
3. STREAMING 응답에서 streamId 파싱
4. BleTransferStreamReceiver가 Channel에서 frame 수신
5. EOF + 전체 검증 → ByteArray 반환

9. ViewModel이 신경 쓰는 것 (화면 관점)

R10SnapshotViewModel:

메커니즘이유
snapshotBleMutexBLE 명령 동시에 두 개 안 날리기
snapshotRequestGeneration오래된 요청 결과 버리기
activeSnapshotSessionId진행 중 session → 필요 시 DROP
retry 최대 3회일시적 BLE 오류 복구
30초 timeout무한 로딩 방지
같은 채널 loading 중 skip중복 CAPTURE 방지

10. 테스트할 때 보면 좋은 Logcat

태그 / 키워드의미
BleAtCommandAT 송수신 (CAPTURE, STREAM, DROP)
BleRepository + transferMode=STREAM고속 경로 성공
transferMode=GET_FALLBACKSTREAM 시작 실패 → GET
R10SnapshotPageFlowViewModel 타이밍 (getSnapshot ms, receiveTransfer ms)

정상 한 장:

CAPTURE → STREAM → (TX-only frame들) → snapshot complete

11. 이번에 실제로 고친 버그 (복습)

문제원인수정
가만히 있는데 CAPTURE 2번STREAM 후 frame 못 받음 → retrySTREAM collector 시작
seq mismatch 즉시 실패첫 frame 유실 (race)Channel 버퍼
binary frame 안 옴CCCD 미설정descriptor write 추가
채널 전환 불가mutex + loading stuck위 fix로 1차 수신 성공

12. 공부 순서 추천

  1. 개념 — 이 문서 1~5장 (BLE, GET vs STREAM, TX-only)
  2. 공개 APIBleRepository.ktgetSnapshot / receiveTransfer KDoc
  3. 연결BleGattConnection.kt (notify 분기, txOnlyBinaryStream)
  4. STREAM 수신BleRepositoryImpl.receiveTransferViaStreamBleTransferStreamReceiverBleBinaryStreamFrameParser
  5. 화면R10SnapshotViewModel.runSnapshotAttempt
  6. 테스트 코드blemanager/src/test/.../transfer/ (frame 파싱 예시)

13. 한 줄 요약

  • CAPTURE = “사진 찍고 주문번호(sessionId) 줘”
  • STREAM = “주문번호로 JPEG를 TX-only로 쏴줘” (빠름)
  • GET = “조각조각 달라고 AT로 계속 요청” (느리지만 호환용)
  • DROP = “그 주문 취소하고 RAM 비워”
  • ViewModelgetSnapshot + receiveTransfer만 부르고, blemanager가 STREAM/GET/DROP/BLE를 처리한다.

작성 기준: 2026-06-29, R10Snapshot STREAM 구현 반영

profile
I am a Blacksmith.

0개의 댓글