NOOP is a standalone, fully offline companion app for WHOOP straps (4.0 and 5.0). It pairs directly with the strap over Bluetooth Low Energy, stores everything on-device in SQLite, imports WHOOP CSV exports and Apple Health exports, and computes recovery / strain / HRV / sleep locally. There is no cloud, no account — the app talks only to your own device and works only with your own data.
Not affiliated with WHOOP, and not a medical device. "WHOOP" is used only to identify the hardware this software interoperates with. NOOP contains no WHOOP code, firmware, or assets. All outputs (HR, HRV, recovery, strain, sleep, SpO₂, temperature) are approximations and are not clinically validated. See
DISCLAIMER.mdandATTRIBUTION.md.
The codebase is split into reusable, cross-platform Swift packages plus a thin platform-specific app layer. The macOS app is the reference implementation; iOS and Android targets are planned and reuse the same packages where they can.
Strand/
├── project.yml # XcodeGen project definition (source of truth for the macOS project)
├── Strand.xcodeproj/ # Generated by `xcodegen generate` — do not hand-edit
├── Strand/ # macOS SwiftUI app target (product name: NOOP)
│ ├── App/ # StrandApp, AppModel, RootView, ContentView
│ ├── BLE/ # CoreBluetooth manager, frame router, commands, live state
│ ├── Collect/ # Backfiller, Collector, clock correlation, store paths
│ ├── Data/ # Repository, importers, profile, notification settings
│ ├── Screens/ # SwiftUI screens (Today, Sleep, Trends, MetricExplorer, …)
│ ├── MenuBar/ # MenuBarExtra content
│ ├── System/ # MacActions (lock screen, run Shortcut), ProjectInfo
│ └── Resources/ # Info.plist, Strand.entitlements, Assets.xcassets (AppIcon)
├── StrandTests/ # macOS app unit tests
├── Packages/
│ ├── WhoopProtocol/ # BLE frame parsing, CRC, command/event/packet decode
│ ├── WhoopStore/ # GRDB/SQLite persistence (migrations, streams, caches)
│ ├── StrandAnalytics/ # HRV / recovery / strain / sleep / correlation math
│ ├── StrandImport/ # WHOOP CSV + Apple Health importers
│ └── StrandDesign/ # SwiftUI design system (palette, components, charts)
├── Tools/
│ └── Backfill/ # `swift run backfill` — re-runs importers into the on-device DB
└── Fixtures/ # Sample data for tests
Every package declares both .iOS(.v16) and .macOS(.v13), so the protocol, storage,
analytics, import, and design layers compile and run unmodified on iOS once an app target exists.
Any framework-specific code is guarded with #if canImport(AppKit) / #elseif canImport(UIKit)
(for example the color bridging in Packages/StrandDesign/Sources/StrandDesign/Palette.swift).
| Package | Platforms | Key dependencies | Responsibility |
|---|---|---|---|
WhoopProtocol |
iOS 16+, macOS 13+ | none (bundles Resources/whoop_protocol.json) |
BLE framing, CRC, command/event/packet decode — the reverse-engineering core |
WhoopStore |
iOS 16+, macOS 13+ | WhoopProtocol, GRDB.swift (≥ 6.0.0) |
SQLite persistence, migrations, decoded streams, metric caches |
StrandAnalytics |
macOS 13+, iOS 16+ | WhoopProtocol, WhoopStore |
HRV / recovery / strain / sleep / correlation math |
StrandImport |
macOS 13+, iOS 16+ | WhoopProtocol, WhoopStore, ZIPFoundation (≥ 0.9.0) |
WHOOP CSV + Apple Health (export.xml, streaming) importers |
StrandDesign |
macOS 13+, iOS 16+ | none | SwiftUI design system: palette, components, charts |
All third-party dependencies are resolved through Swift Package Manager; nothing is vendored as a binary.
| Tool | Version used in this repo | Notes |
|---|---|---|
| macOS | 13 (Ventura) or newer | Deployment target is macOS 13.0 |
| Xcode | 26.x (Swift 6.3 toolchain) | Provides xcodebuild and the macOS SDK |
| XcodeGen | 2.45+ | Generates Strand.xcodeproj from project.yml |
Install XcodeGen via Homebrew:
brew install xcodegenThe packages themselves only need a Swift toolchain — they build and test with plain swift build
/ swift test, no Xcode project required.
The macOS project is generated, not committed by hand. project.yml is the source of truth;
Strand.xcodeproj is produced from it. Re-run generation whenever you add/remove source files or
change project.yml.
cd /path/to/Strand
xcodegen generatexcodebuild \
-project Strand.xcodeproj \
-scheme Strand \
-destination 'platform=macOS' \
CODE_SIGNING_ALLOWED=NO \
buildNotes on the build:
- The Xcode scheme is
Strand(the target is still namedStrandinternally), but the built product isNOOP.app—project.ymlsetsPRODUCT_NAME: NOOPand bundle idcom.noopapp.noop, with display nameNOOP. CODE_SIGNING_ALLOWED=NOskips signing for a fast local compile-and-verify loop. To produce a runnable.appyou instead want an ad-hoc-signed build (see below).- SPM resolves
GRDB.swiftandZIPFoundationon first build intobuild/SourcePackages/.
The app is sandboxed and requests Bluetooth + user-selected-file access. From
Strand/Resources/Strand.entitlements:
<key>com.apple.security.app-sandbox</key> <true/>
<key>com.apple.security.device.bluetooth</key> <true/>
<key>com.apple.security.files.user-selected.read-write</key> <true/>project.yml deliberately keeps DEVELOPMENT_TEAM empty, ENABLE_HARDENED_RUNTIME: NO, and uses
ad-hoc signing — no Apple Developer account is required to run a personal build. To produce the
runnable bundle, build without disabling signing so Xcode applies the sandbox + Bluetooth
entitlements with an ad-hoc identity:
xcodebuild \
-project Strand.xcodeproj \
-scheme Strand \
-configuration Debug \
-destination 'platform=macOS' \
-derivedDataPath build \
CODE_SIGN_IDENTITY="-" \
buildThe product lands at build/Build/Products/Debug/NOOP.app. You can confirm it is ad-hoc signed:
codesign -dvv build/Build/Products/Debug/NOOP.app
# Signature=adhoc
# Identifier=com.noopapp.noop
# CodeDirectory ... flags=0x2(adhoc)
# TeamIdentifier=not setCopy the bundle wherever you like (e.g. ~/Desktop/NOOP.app) and double-click to launch. On first
run macOS prompts for Bluetooth permission — the prompt text comes from
NSBluetoothAlwaysUsageDescription in the Info.plist and explains that data stays on-device.
NOOP connects over CoreBluetooth (Strand/BLE/BLEManager.swift). The central manager is created
on the main queue:
central = CBCentralManager(delegate: self, queue: .main)so all delegate callbacks arrive on the main actor. WHOOP 4.0 uses the 61080001-… service family
with a CRC8 header; WHOOP 5.0 (the "goose"/MG path) uses the fd4b0001-… family with a
CRC16-Modbus header. The strap must be out of range of the official app during initial bonding,
and worn/charged to report a non-zero heart rate.
# after editing project.yml or adding/removing files:
xcodegen generate
# fast syntax/type check (no signing, no bundle):
xcodebuild -project Strand.xcodeproj -scheme Strand \
-destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO build
# per-package iteration (much faster than the full app):
cd Packages/WhoopProtocol && swift build && swift test
cd Packages/WhoopStore && swift build && swift test
cd Packages/StrandImport && swift build && swift testYou can also open the generated project interactively:
open Strand.xcodeprojThe on-device SQLite database lives inside the app sandbox container:
~/Library/Containers/com.noopapp.noop/Data/.../whoop.sqlite
Tools/Backfill is a small executable that re-runs the WHOOP CSV / Apple Health import mapping
directly against that database — useful after changing importer logic:
cd Tools/Backfill
swift run backfillAll five packages already target .iOS(.v16), so the non-UI core compiles for iOS today.
There is currently no iOS app target; adding one is mostly app-layer wiring, not package work.
-
Reuse the packages directly. In
project.yml, add an iOS application target that depends on the same packages the macOS target uses:targets: StrandiOS: type: application platform: iOS deploymentTarget: "16.0" sources: [StrandiOS] # iOS-specific app layer dependencies: - package: WhoopProtocol - package: WhoopStore - package: StrandAnalytics - package: StrandImport - package: StrandDesign
Then
xcodegen generateand build with-scheme StrandiOS -destination 'generic/platform=iOS'(or a simulator destination).WhoopProtocol,WhoopStore,StrandAnalytics,StrandImport, and most ofStrandDesignneed no changes. -
CoreBluetooth on iOS.
BLEManageralready uses CoreBluetooth, which is identical API on iOS. The differences are:- Add
NSBluetoothAlwaysUsageDescriptionto the iOS Info.plist (the macOS one already exists). - For background offload, request the
bluetooth-centralbackground mode and consider CoreBluetooth state restoration —BLEManageralready handlesCBCentralManagerRestoredStatePeripheralsKey, so restoration is wired but the iOS background-modes entitlement must be added. - Replace the macOS app-sandbox +
com.apple.security.device.bluetoothentitlements with the iOS signing/capabilities equivalents.
- Add
-
App-layer code that needs an iOS variant. The packages are clean; the macOS app directory has a handful of AppKit dependencies that must be ported (or
#if os(macOS)-gated) when building the iOS app:macOS app code File iOS replacement NSPasteboard.general(copy donation address)Strand/Screens/SupportView.swiftUIPasteboard.generalNSWorkspace.shared.open(url:)/.icon(forFile:)Strand/System/MacActions.swift,Strand/Data/NotificationSettingsStore.swiftUIApplication.shared.open(_:); app icons aren't available on iOSNSImagefor app iconsStrand/Data/NotificationSettingsStore.swiftUIImage(and rethink the macOS-only notification-mirroring feature)MenuBarExtra+MenuBarContent(menu-bar HR)Strand/App/StrandApp.swift,Strand/MenuBar/No menu bar on iOS — use a widget / Live Activity instead MacActions.lockScreen()(login.framework) andrunShortcut(_:)Strand/System/MacActions.swiftmacOS-only; the strap-double-tap actions have no direct iOS analogue .windowStyle(.hiddenTitleBar)/.defaultSizewindow chromeStrand/App/StrandApp.swiftDrop window modifiers; use a normal iOS scene Because the design system (
StrandDesign) already bridgesNSColor/UIColorbehind#if canImport(AppKit) / #elseif canImport(UIKit), the palette, fonts, and most components carry over without edits.
A native Android client is planned as a separate, Kotlin/Gradle module rather than a port of the
Swift app. When present, it lives under android/ with its own README.
Expected toolchain:
| Tool | Version |
|---|---|
| JDK | 17 |
| Android Studio | current stable (with Android SDK) |
| Build system | Gradle (Android Gradle Plugin) |
The Android app re-implements the same wire protocol against Android's BLE stack (the protocol
facts in WhoopProtocol/Resources/whoop_protocol.json are language-agnostic). Build and run
instructions live in android/README.md — open the android/ directory in Android Studio, let
Gradle sync, and run on a device with Bluetooth (an emulator cannot reach a physical strap).
The
android/directory may not yet exist in your checkout. Until it lands, the macOS app above is the reference implementation and the shared packages define the protocol, storage, analytics, and import behavior any future client must match.
# Package test suites (no Xcode project needed):
cd Packages/WhoopProtocol && swift test
cd Packages/WhoopStore && swift test
cd Packages/StrandAnalytics && swift test
cd Packages/StrandImport && swift test
cd Packages/StrandDesign && swift test
# macOS app + integration tests via Xcode:
xcodegen generate
xcodebuild -project Strand.xcodeproj -scheme Strand -destination 'platform=macOS' testNOOP builds on prior open-source reverse-engineering and interoperability work:
johnmiddleton12/my-whoop— WHOOP 4.0 BLE protocol; theWhoopProtocolandWhoopStorepackages are adapted from this work.b-nnett/goose— WHOOP 5.0 / MG BLE protocol (service familyfd4b0001-…, CRC16-Modbus header) that the WHOOP-5 decode path is ported from.groue/GRDB.swift— SQLite persistence.weichsel/ZIPFoundation— zip handling for Apple Health exports.
See ATTRIBUTION.md for full detail.