왜 만들었나
브랜드 앱은 라이브뷰와 Wi-Fi 연결을 거쳐야 해서 셔터 한 번 누르기까지 오래 걸립니다. 카메라에 이미 있는 BLE 리모컨 프로토콜(RMT-P1BT, BR-E1, ML-L7)을 흉내 내 연결 시간을 줄였습니다.
주요 기능
- 광고 패킷의 제조사 데이터·서비스 UUID만으로 브랜드와 페어링 대기 상태 판별
- 누르는 순간 셔터 누름, 떼는 순간 뗌: 탭은 한 장, 길게 누르면 연사·벌브
- 등록한 여러 대를 동시에 촬영, 셀프타이머 2/5/10초
- 카메라 전원만 켜면 자동 재연결(백그라운드 블루투스, 상태 복원)
- 소니는 AF가 끝나 셔터가 실제로 열릴 때까지 버튼을 유지하고 촬영 시 진동
숫자로 보면
3개 브랜드, 드라이버 4종(소니 RMT-P1BT · 캐논 BR-E1 · 캐논 스마트폰 모드 · 니콘 ML-L7)
자동 재연결 최대 8회, 간격 3초→최대 300초 지수 백오프
Swift 파일 17개, 약 2,250줄
어떻게 동작하나
스캔·판별
CBCentralManager로 스캔하며 회사 ID(0x012D·0x01A9·0x0399)와 서비스 UUID로 카메라 종류와 페어링 가능 여부를 가립니다.
ShutterLink/Cameras/AdvertisementParser.swiftGATT 래핑
서비스 검색·쓰기·알림 구독을 async/await와 타임아웃이 있는 OneShot으로 감싸 드라이버가 순차 코드로 쓰게 합니다.
ShutterLink/BLE/PeripheralLink.swift브랜드 핸드셰이크
CameraDriver 프로토콜의 setUp에서 소니는 FF02 알림 구독, 캐논은 리모컨 이름 등록, 니콘은 4단계 인증을 수행합니다.
ShutterLink/Cameras/NikonDriver.swift셔터 명령 직렬화
카메라마다 명령을 Task 체인에 쌓아 순서대로 보내고, 최소 유지 시간은 카메라가 누름을 받은 시점부터 잽니다.
ShutterLink/BLE/CameraSession.swift동시 촬영
셔터 버튼을 누르면 체크된 채 준비된 세션 전체에 pressShutter를, 떼면 releaseShutter를 보냅니다.
ShutterLink/BLE/BLEController.swift자동 재연결
끊기면 3초부터 두 배씩 최대 300초 간격으로 최대 8회 다시 연결하고, 니콘은 재연결 광고를 받기 위해 스캔을 새로 시작합니다.
ShutterLink/BLE/BLEController.swift
핵심 코드
// Stage 1 → 2 (카메라가 stage 0을 보내면 stage 1을 한 번 다시 보낸다 — furble과 동일)
let stage1: [UInt8] = [0x01] + [0, 0, 0, 0, 0, 0, 0, 0x01] + identity
var r2 = try await exchange(stage1, on: pair, link: link, label: "니콘 인증 2단계")
if r2.first == 0x00 {
r2 = try await exchange(stage1, on: pair, link: link, label: "니콘 인증 2단계")
}
guard r2.first == 0x02 else {
throw BLEError.protocolError("니콘 인증 2단계 응답이 예상과 다릅니다: \(r2.hex)")
}
// Stage 3 → 4
let stage3: [UInt8] = [0x03] + Array(repeating: 0, count: 16)
let r4 = try await exchange(stage3, on: pair, link: link, label: "니콘 인증 4단계")
guard r4.first == 0x04 else {
throw BLEError.protocolError("니콘 인증 4단계 응답이 예상과 다릅니다: \(r4.hex)")
}니콘 ML-L7 리모컨 모드의 4단계 인증: 17바이트 메시지를 보내고 인디케이션 응답의 단계 바이트를 확인합니다.
어려웠던 점
제조사 공개 문서가 없어 furble·freemote 등 역공학 자료를 바탕으로 브랜드마다 다른 순서(소니의 반누름→완전누름→완전뗌→반뗌, 니콘 인증 핸드셰이크)를 맞춰야 했습니다. 응답을 놓치지 않도록 쓰기 전에 응답 대기를 먼저 걸어 두는 방식으로 처리했습니다.