WhoopProtocol과, 논문 기반 알고리즘으로 점수를 다시 계산하는 StrandAnalytics입니다. (NoopApp/noop · Swift 50.7% + Kotlin 45.4% (Python 3.7%·Shell 0.2%) · v4.2.11 · WHOOP 비공식·비제휴)WHOOP은 심박·HRV·수면을 재는 손목/팔 밴드입니다. 그런데 이 밴드는 화면이 없고, 데이터를 보려면 반드시 WHOOP의 공식 앱으로 로그인해 클라우드에 올리고 월 구독료를 내야 합니다. 즉 "내 몸에서 나온 데이터"인데도, 그걸 보려면 회사 서버와 구독을 거쳐야 하는 구조입니다.
NOOP은 그 구조를 뒤집습니다. 밴드와 블루투스로 직접 통신해서 심박·R-R 간격·SpO₂·피부온도·호흡·가속도 같은 원시 데이터를 빼내고, 전부 내 컴퓨터/폰의 로컬 SQLite 파일에 저장합니다. 회복(Recovery)·스트레인(Strain)·HRV·수면 점수도 서버가 아니라 기기 안에서 직접 계산합니다. 네트워크를 쓰는 기능은 (선택 사항인) AI 코치 하나뿐이고, 그것도 내 API 키로만 동작합니다.
정비소(클라우드)에 차를 맡기지 않아도, OBD 포트에 직접 진단기를 꽂으면 엔진 데이터를 내가 바로 읽을 수 있죠. NOOP은 WHOOP 밴드의 블루투스 단자에 직접 꽂아 데이터를 읽는 진단기입니다.
다른 점은, WHOOP은 공식 단자 규격을 공개하지 않았다는 것. 그래서 NOOP은 통신 신호를 하나하나 뜯어보며 규격을 알아낸(역분석한) 결과를 코드로 구현했습니다. 잠금 해제나 결제 우회가 아니라, "내가 가진 기기와 그냥 대화하는" 상호운용(interoperability) 프로젝트입니다.
핵심 트렌딩 이유는 두 가지입니다. ① "내 데이터 주권"이라는 정서 — 하드웨어는 샀는데 데이터는 구독으로 인질 잡힌 상황에 대한 반발이 큽니다. ② 기술적 완성도 — 단순한 데이터 덤프 도구가 아니라, WHOOP 4.0과 5.0 두 세대의 프로토콜을 모두 다루고, 회복/스트레인/수면 점수를 논문 기반으로 직접 재계산하며, macOS·Android 풀 앱을 같은 로직으로 포팅한 진짜 제품이라는 점입니다.
| 항목 | 공식 WHOOP 앱 | NOOP |
|---|---|---|
| 데이터 저장 | WHOOP 클라우드 | ○ 내 기기 로컬 SQLite |
| 계정 / 로그인 | 필수 | ○ 불필요 (서버 자체가 없음) |
| 구독료 | 월 정기 결제 | ○ 무료 (기부는 100% 선택) |
| 오프라인 동작 | 제한적(동기화 필요) | ○ 완전 오프라인 |
| 점수 계산 위치 | 서버 | ○ 기기 안에서 (투명한 수식) |
| 기존 기록 가져오기 | — | ○ WHOOP CSV + Apple Health 임포트 |
| 플랫폼 | iOS / Android | macOS · Android · iOS(사이드로드, App Store 미출시) |
| 알고리즘 공개 | ✕ (독점) | ○ 파일마다 근거 논문 명시 |
공식 앱의 회복 점수는 블랙박스입니다. NOOP은 회복·스트레인·HRV·수면을 출처가 명시된 공개 방법으로 다시 계산합니다 — HRV는 Task Force 1996의 RMSSD/SDNN, 스트레인은 Karvonen %HRR과 Edwards/Banister TRIMP, 최대심박은 Tanaka 공식. 각 분석기 파일이 "무엇을 어떻게 계산하는지"를 스스로 문서화합니다. (독점 모델의 재현이 아니라 근사임을 명시.)
밴드는 HRV를 측정할 뿐 아니라 진동 모터로 신호도 보낼 수 있습니다. NOOP은 이를 활용해 화면 없이 쓰는 기능을 만들었습니다 — Breathe(들숨 1번·날숨 2번 진동으로 호흡 페이싱하는 HRV 바이오피드백), Intervals(구간 전환마다 진동하는 무음 HIIT 타이머). 측정+피드백을 한 기기로 닫는 설계가 돋보입니다.
이미 공식 앱이나 Apple Health에 수년치 데이터가 있다면, WHOOP CSV 내보내기와 Apple Health export.xml을 한 번 임포트하면 전부 내 기기로 옮겨집니다. 특히 1GB가 넘을 수 있는 export.xml을 스트리밍 SAX 파서로 처리해 메모리 폭발 없이 읽어 들이는 점이 실전적입니다.
NOOP은 "데이터 탈취 도구"가 아니라 "내가 산 센서를 내가 통제하는 기기에서 읽는 자유"를 코드로 구현한 사례다. 그래서 기술(BLE 역분석·on-device 분석)과 철학(데이터 주권)이 동시에 화제가 됐다.
NOOP의 설계 철학은 "플랫폼에 의존하지 않는 순수 로직 패키지 + 얇은 UI 앱"입니다. 핵심 로직(프로토콜 디코드·저장·분석)은 플랫폼-순수(platform-pure) Swift 패키지로 짜여 테스트·CLI에서도 그대로 돌고, 같은 로직을 Android용으로 Kotlin에 다시 포팅했습니다. UI만 macOS는 SwiftUI, Android는 Jetpack Compose로 입힙니다.
| 항목 | 내용 |
|---|---|
| 언어 | Swift 50.7%(macOS·iOS·공용 로직) + Kotlin 45.4%(Android) + Python 3.7%·Shell 0.2% — 같은 알고리즘의 2개 포팅 |
| macOS UI | SwiftUI (macOS 13+), 메뉴바 엑스트라 + 온보딩 마법사 포함 |
| Android UI | Jetpack Compose (Android 8+ / minSdk 26) |
| 프로젝트 생성 | XcodeGen — project.yml 한 장에서 Strand.xcodeproj 생성 |
| 패키지 관리 | Swift Package Manager (SPM) |
| 코드사이닝 | 샌드박스 + Hardened Runtime, 유니버설 바이너리(Apple Silicon+Intel) |
| 라이선스 | 비상업·비공식 상호운용 프로젝트 (WHOOP 비제휴) |
놀랍도록 의존성이 적습니다. UI 라이브러리조차 직접 만들어 썼습니다(StrandDesign).
| 패키지 | 역할 |
|---|---|
| GRDB.swift | SQLite를 Swift답게 다루는 ORM/쿼리 라이브러리 — 로컬 저장의 전부 |
| ZIPFoundation | WHOOP CSV 내보내기 .zip 압축 해제 |
| 패키지 | 하는 일 |
|---|---|
| WhoopProtocol | BLE 프레임 파싱·CRC 검증·명령/이벤트/패킷 디코드 (역분석 핵심) |
| WhoopStore | GRDB/SQLite 영속화 — 마이그레이션·스트림·캐시 |
| StrandAnalytics | HRV·회복·스트레인·수면·상관 분석 수학 (DB 없는 순수 함수) |
| StrandImport | WHOOP CSV + Apple Health 임포터 |
| StrandDesign | SwiftUI 디자인 시스템 (팔레트·차트·컴포넌트, 외부 UI 의존 0) |
| NoopLocalAccess | 로컬 SQLite를 외부 도구·스크립트에서 직접 읽을 수 있게 해 주는 CLI 라이브러리(noop-local-access 실행 파일, macOS 전용, GRDB 기반) |
WhoopProtocol은 CoreBluetooth를 임포트하지 않는다. 즉 실제 블루투스 하드웨어가 없어도, 저장해 둔 바이트 프레임만으로 디코드 로직을 단위 테스트할 수 있다. BLE 연결·본딩 같은 플랫폼 부분은 앱 레이어(Strand/BLE/)가 UUID 문자열을 CBUUID로 감싸 처리한다. "순수 로직 ↔ 플랫폼 어댑터"를 분리하는 교과서적 구조다.
project.yml(YAML)에서 .xcodeproj를 생성해 주는 도구. .xcodeproj는 사람이 손으로 편집하기 어렵고 git 충돌이 잦은데, XcodeGen을 쓰면 프로젝트 설정을 읽기 쉬운 텍스트로 버전 관리하고 명령 한 줄(xcodegen generate)로 재생성할 수 있다.NOOP의 진짜 배울 점은 "센서 → 디코드 → 저장 → 분석 → UI"의 단방향 파이프라인을 플랫폼 독립적으로 설계한 방식입니다. 모든 데이터는 한 방향으로 흐르고, 각 단계가 명확히 분리된 패키지입니다. 그리고 모든 화살표가 내 기기 안에서만 일어납니다.
WHOOP 4.0과 5.0은 통신 규격이 다릅니다. NOOP은 이를 DeviceFamily 한 타입으로 추상화해, 같은 디코더가 세대별 차이만 분기하도록 했습니다.
public enum DeviceFamily: String, Sendable, CaseIterable {
case whoop4 // CRC8(poly 0x07) 헤더 검사, 서비스 61080001-…
case whoop5 // CRC16-Modbus 헤더 검사, "puffin" 패킷, 서비스 fd4b0001-…
}
디코딩은 스키마 주도(schema-driven)입니다. Resources/whoop_protocol.json이 프레임 구조를 데이터로 기술하고, 코드는 그 스키마대로 바이트를 해석합니다. CRC8·CRC16-Modbus·zlib CRC-32 구현, 프레임 프레이밍, 값 해석, 과거 스트림 재조립이 여기 들어 있습니다. 새 필드가 생기면 코드가 아니라 JSON을 고칩니다.
CRC8, 5는 CRC16-Modbus를 쓰는데, NOOP은 두 세대를 구분하는 단서로도 이 CRC 방식 차이를 활용한다.WhoopStore의 스키마는 v1부터 현재 v16(v16-paired-device-peripheral)까지 누적된 마이그레이터입니다. 앱 업데이트에 따라 계속 증가하며, 기존 로컬 DB가 단계적으로 최신 스키마로 올라갑니다. v1~v3의 디코드 스트림 테이블 예:
CREATE TABLE hrSample (deviceId TEXT, ts INTEGER, bpm INTEGER, ...);
CREATE TABLE rrInterval (deviceId TEXT, ts INTEGER, rrMs INTEGER, ...);
CREATE TABLE spo2Sample (deviceId TEXT, ts INTEGER, red INTEGER, ir INTEGER, ...);
CREATE TABLE skinTempSample(deviceId TEXT, ts INTEGER, raw INTEGER, ...);
CREATE TABLE respSample (deviceId TEXT, ts INTEGER, raw INTEGER, ...);
이후 마이그레이션은 서버 없이 기기가 계산한 지표 캐시(sleepSession, dailyMetric), 커서, 원시 프레임 아웃박스 등을 추가합니다.
StrandAnalytics는 DB도 모르는 순수 함수 모음입니다. 입력은 숫자 배열, 출력은 점수. 각 파일이 근거 논문을 명시합니다.
| 파일 | 계산하는 것 |
|---|---|
| HRVAnalyzer | R-R 간격에서 RMSSD+SDNN (Task Force 1996), 범위·Malik 이소성 필터 |
| RecoveryScorer | 0~100 회복 점수: HRV 중심 z-score + 개인 기준선 대비 로지스틱 합성 |
| StrainScorer | 0~21 로그 스트레인: %HRR(Karvonen) + Edwards/Banister TRIMP |
| SleepStager | 심폐+중력 특징으로 수면/각성 + 근사 4단계 staging |
| CorrelationEngine | Pearson r, OLS 회귀, 시차(lag) 상관 — 두 지표 사이 관계 |
| WorkoutDetector 외 | 운동 감지·롤링 기준선·행동 인사이트·일일 오케스트레이터 |
"순수 수학 함수"로 떼어 놓으면, 같은 RMSSD 계산을 Swift와 Kotlin에서 똑같이 옮길 수 있고, 고정된 입력으로 결과를 못 박아 테스트할 수 있다. UI·DB·블루투스에 얽힌 코드와 섞였다면 절대 못 할 일이다. NOOP이 두 플랫폼에서 "같은 점수"를 보장하는 비결이 이 분리다.
Strand/Screens/AutomationsView.swift + System/MacActions.swift는 밴드의 물리 입력과 실시간 생체값을 Mac 동작으로 바꿉니다.
| 트리거 | 동작 |
|---|---|
| 밴드 더블탭 | Mac 잠금 / 순간 표시 / 임의의 macOS 단축어 실행(shortcuts:// URL — 샌드박스 친화적) |
| 착용/이탈 감지 | 밴드를 벗으면 Mac 잠금, 다시 차면 단축어 실행 (※ 자동 잠금해제는 Apple Watch 전용이라 불가) |
| 심박 존 / 휴식 스트레스 | 햅틱 코칭 — 화면 안 봐도 진동으로 알림 |
| 스마트 알람 | 밴드 펌웨어 알람을 직접 무장 — Mac이 자거나 앱이 꺼져도 기상 시각에 진동 |
"순수 로직 패키지 + 얇은 앱 타깃"이라는 철학이 폴더 구조에 그대로 드러납니다. Strand/가 빌드하는 macOS 앱이고, 진짜 두뇌는 Packages/ 아래에 흩어져 있습니다.
처음 본다면 README → project.yml(앱이 어떤 패키지를 묶는지) → WhoopProtocol의 DeviceFamily와 whoop_protocol.json(데이터가 어떻게 생겼나) → WhoopStore/Database.swift(어디에 저장되나) → StrandAnalytics/HRVAnalyzer.swift(가장 단순한 분석기) → Strand/App/RootView.swift(UI가 어떻게 엮이나) 순서가 좋다.
웨어러블·IoT의 기본기입니다. "서비스/특성 UUID → notify 구독 → 바이트 프레임 → 의미 있는 값"으로 이어지는 흐름과, CRC로 무결성을 확인하고 스키마로 필드를 해석하는 패턴을 한 레포에서 통째로 볼 수 있습니다.
Web Bluetooth API로 가까운 BLE 기기를 스캔해 서비스·특성 UUID 목록을 출력하는 30줄짜리 페이지를 만들어 본다.WhoopProtocol이 CoreBluetooth를 임포트하지 않는다는 한 가지 결정이, 테스트 가능성·재사용성·멀티플랫폼 포팅을 전부 가능하게 했습니다. 의존성을 안쪽(순수)이 아니라 바깥쪽(앱)이 떠안게 하는 의존성 역전의 실전 예시입니다.
클라우드 없이 SQLite 한 파일에 모든 것을 담고, DB 변화를 관찰해 UI를 갱신하며, 스키마를 버전 마이그레이션으로 진화시키는 방식. 오프라인 앱·프라이버시 앱 설계의 정석입니다.
v1→v2로 올릴 때 기존 데이터가 보존되는 미니 앱을 만든다.RMSSD(연속 R-R 차이의 제곱평균제곱근), z-score 기준선, TRIMP 같은 공식이 어떻게 코드가 되는지 볼 수 있습니다. "논문 한 줄 → 함수 한 개"로 옮기는 연습으로 최고의 교본입니다.
HRVAnalyzer 결과와 같은 입력으로 비교해 본다.Apple Health export.xml은 통째로 메모리에 올리면 죽습니다. AppleHealthImporter는 SAX(이벤트 기반) 파서로 한 요소씩 흘려보내며 처리합니다. 대용량 데이터 처리의 핵심 기술입니다.
| 항목 | 요구사항 |
|---|---|
| macOS 앱 | macOS 13+, 블루투스 있는 Mac. 빌드 시 Xcode 15+ (Swift 5.9) + XcodeGen |
| Android 앱 | Android 8+ (minSdk 26). APK 사이드로드(\"알 수 없는 앱 설치\" 허용) |
| iOS | iOS 17+. AltStore/SideStore로 IPA 사이드로드. App Store 미출시, 본인 Apple ID로 서명. (패키지 플랫폼 지원은 iOS 16+) |
| 실시간 페어링 | 본인 소유의 WHOOP 밴드(4.0 또는 5.0) |
| 밴드 없이 체험 | WHOOP CSV / Apple Health 임포트로 탐색 가능 (데모 APK는 샘플 데이터 내장) |
| 네트워크 | 불필요 — 유일한 예외는 선택적 AI 코치(본인 API 키) |
| 권한(entitlements) | app-sandbox · 블루투스 · 사용자 선택 파일 읽기쓰기. Hardened Runtime ON |
NOOP은 WHOOP과 무관한 비공식·비제휴 상호운용 프로젝트다. \"WHOOP\"은 대화 상대인 하드웨어를 가리키기 위해서만 쓰였고, 독점 코드·펌웨어·로고를 포함하지 않으며 로그인/페이월/DRM을 우회하지 않는다. 또한 의료기기가 아니다 — 심박·HRV·회복·수면·SpO₂ 등 모든 값은 공개 방법으로 계산한 근사치이며 임상 검증되지 않았다. 진단·치료·건강 의사결정에 쓰지 말 것. 본인이 가진 밴드가 있어야 실시간 데이터를 받을 수 있다.
릴리스에서 NOOP-demo.apk(샘플 데이터 내장)를 설치하거나, macOS 앱을 빌드한 뒤 Fixtures/의 샘플 WHOOP 내보내기를 Data Sources로 임포트한다. Today·Sleep·Trends 화면이 어떤 지표를 보여 주는지, 어디서 그 값이 왔는지 데이터 출처 푸터를 읽는다.
project.yml을 열어 Strand 앱 타깃이 어떤 패키지 5개에 의존하는지, 각 패키지는 또 무엇에 의존하는지 화살표로 그려 본다. \"앱은 무겁고 패키지는 가볍다\"는 의존성 방향을 눈으로 확인한다.
Xcode 프로젝트 없이 cd Packages/WhoopProtocol && swift build && swift test를 돌려 본다. CoreBluetooth 없이도 디코드 테스트가 통과하는 걸 확인하고, whoop_protocol.json 스키마와 디코더 코드가 어떻게 짝지어지는지 따라간다.
StrandAnalytics/HRVAnalyzer.swift의 RMSSD 계산을 읽고, 같은 알고리즘을 파이썬(또는 본인 언어)으로 다시 짠다. 동일한 R-R 배열을 넣어 결과가 일치하는지 검증한다 — \"순수 함수가 왜 멀티플랫폼에 강한가\"를 체감하는 과제.
System/MacActions.swift의 shortcuts:// 호출 패턴을 본떠, \"밴드를 더블탭하면 특정 macOS 단축어 실행\"에 새 동작을 하나 추가한다. 샌드박스 안에서 외부 동작을 안전하게 트리거하는 방법을 익힌다.
| 주차 | 주제 | 할 일 |
|---|---|---|
| 1주차 | BLE / 바이너리 프로토콜 | GATT(서비스·특성·notify) 개념, Web Bluetooth로 스캔 실습, CRC 체크섬 직접 계산 |
| 2주차 | 로컬 우선 데이터 저장 | SQLite + GRDB(또는 Room) 익히기, 버전 마이그레이션, DB 변화 관찰로 UI 갱신 |
| 3주차 | 생체신호 분석 수학 | HRV(RMSSD/SDNN), %HRR·TRIMP, z-score 기준선, 상관/회귀를 코드로 옮기기 |
| 4주차 | 멀티플랫폼 + 대용량 처리 | 순수 로직을 Swift↔Kotlin 포팅, SAX 스트리밍 파서로 대용량 XML 처리, 샌드박스 권한 |
이 레포는 \"웨어러블 데이터 앱을 어떻게 처음부터 끝까지 만드나\"의 완결된 예제다. 센서에서 바이트를 읽는 가장 낮은 층부터, 점수를 계산하는 수학, 화면에 그리는 UI까지 한 줄로 꿰여 있다. 한 층씩 \"이 층의 입력과 출력은 무엇인가\"를 적어 나가면, 4주 뒤엔 다른 센서(스마트워치·체중계 등)로도 같은 골격을 옮길 수 있다.
rrInterval 테이블에 저장하고, Breathe 화면에서 호흡에 따라 변하는 RMSSD를 실시간으로 보여 준다.fd4b0001-…) 아래에 여러 \"특성\"을 두고, 앱이 특성에 notify를 켜면 값이 바뀔 때마다 받는다. NOOP은 WHOOP 4/5의 서비스 UUID를 단서로 세대를 구분하고 올바른 디코더를 고른다.export.xml도 메모리 폭발 없이 읽을 수 있다. NOOP의 AppleHealthImporter가 이 방식을 쓴다.DYLD_INSERT_LIBRARIES) 같은 공격 통로를 막는 보안 강화 모드. NOOP은 둘 다 켜서, \"내 데이터만 다루고 밖으로 안 샌다\"는 약속을 OS 수준으로 뒷받침한다.