사진·카메라

셔터링크

아이폰이 소니·캐논·니콘 순정 블루투스 리모컨인 척 카메라에 붙어 셔터만 빠르게 누르는 iOS 앱.

분야사진·카메라플랫폼iOS 17+기술Swift · SwiftUI · CoreBluetooth · Swift Concurrency (async/await) · UserDefaults
BLE GATT writeiPhone · 리모컨 흉내Sony · Canon · Nikon 셔터
저장소에 공개된 화면 이미지가 없어, 동작 원리를 그림으로 대신합니다.

왜 만들었나

브랜드 앱은 라이브뷰와 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줄

어떻게 동작하나

  1. 스캔·판별

    CBCentralManager로 스캔하며 회사 ID(0x012D·0x01A9·0x0399)와 서비스 UUID로 카메라 종류와 페어링 가능 여부를 가립니다.

    ShutterLink/Cameras/AdvertisementParser.swift
  2. GATT 래핑

    서비스 검색·쓰기·알림 구독을 async/await와 타임아웃이 있는 OneShot으로 감싸 드라이버가 순차 코드로 쓰게 합니다.

    ShutterLink/BLE/PeripheralLink.swift
  3. 브랜드 핸드셰이크

    CameraDriver 프로토콜의 setUp에서 소니는 FF02 알림 구독, 캐논은 리모컨 이름 등록, 니콘은 4단계 인증을 수행합니다.

    ShutterLink/Cameras/NikonDriver.swift
  4. 셔터 명령 직렬화

    카메라마다 명령을 Task 체인에 쌓아 순서대로 보내고, 최소 유지 시간은 카메라가 누름을 받은 시점부터 잽니다.

    ShutterLink/BLE/CameraSession.swift
  5. 동시 촬영

    셔터 버튼을 누르면 체크된 채 준비된 세션 전체에 pressShutter를, 떼면 releaseShutter를 보냅니다.

    ShutterLink/BLE/BLEController.swift
  6. 자동 재연결

    끊기면 3초부터 두 배씩 최대 300초 간격으로 최대 8회 다시 연결하고, 니콘은 재연결 광고를 받기 위해 스캔을 새로 시작합니다.

    ShutterLink/BLE/BLEController.swift

핵심 코드

ShutterLink/Cameras/NikonDriver.swiftGitHub에서 보기 ↗
// 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 등 역공학 자료를 바탕으로 브랜드마다 다른 순서(소니의 반누름→완전누름→완전뗌→반뗌, 니콘 인증 핸드셰이크)를 맞춰야 했습니다. 응답을 놓치지 않도록 쓰기 전에 응답 대기를 먼저 걸어 두는 방식으로 처리했습니다.