Skip to content

Latest commit

 

History

History
324 lines (249 loc) · 13.9 KB

File metadata and controls

324 lines (249 loc) · 13.9 KB

Building NOOP

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.md and ATTRIBUTION.md.


Repository layout

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

Packages and platforms

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.


Prerequisites

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 xcodegen

The packages themselves only need a Swift toolchain — they build and test with plain swift build / swift test, no Xcode project required.


macOS build & run (reference implementation)

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.

1. Generate the Xcode project

cd /path/to/Strand
xcodegen generate

2. Build

xcodebuild \
  -project Strand.xcodeproj \
  -scheme Strand \
  -destination 'platform=macOS' \
  CODE_SIGNING_ALLOWED=NO \
  build

Notes on the build:

  • The Xcode scheme is Strand (the target is still named Strand internally), but the built product is NOOP.appproject.yml sets PRODUCT_NAME: NOOP and bundle id com.noopapp.noop, with display name NOOP.
  • CODE_SIGNING_ALLOWED=NO skips signing for a fast local compile-and-verify loop. To produce a runnable .app you instead want an ad-hoc-signed build (see below).
  • SPM resolves GRDB.swift and ZIPFoundation on first build into build/SourcePackages/.

3. The ad-hoc-signed NOOP.app

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="-" \
  build

The 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 set

Copy 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.

4. Pairing & BLE on macOS

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.

5. Dev loop

# 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 test

You can also open the generated project interactively:

open Strand.xcodeproj

6. Re-importing data into the on-device DB

The 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 backfill

iOS (planned — packages are ready)

All 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.

Adding an iOS app target

  1. 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 generate and build with -scheme StrandiOS -destination 'generic/platform=iOS' (or a simulator destination). WhoopProtocol, WhoopStore, StrandAnalytics, StrandImport, and most of StrandDesign need no changes.

  2. CoreBluetooth on iOS. BLEManager already uses CoreBluetooth, which is identical API on iOS. The differences are:

    • Add NSBluetoothAlwaysUsageDescription to the iOS Info.plist (the macOS one already exists).
    • For background offload, request the bluetooth-central background mode and consider CoreBluetooth state restorationBLEManager already handles CBCentralManagerRestoredStatePeripheralsKey, so restoration is wired but the iOS background-modes entitlement must be added.
    • Replace the macOS app-sandbox + com.apple.security.device.bluetooth entitlements with the iOS signing/capabilities equivalents.
  3. 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.swift UIPasteboard.general
    NSWorkspace.shared.open(url:) / .icon(forFile:) Strand/System/MacActions.swift, Strand/Data/NotificationSettingsStore.swift UIApplication.shared.open(_:); app icons aren't available on iOS
    NSImage for app icons Strand/Data/NotificationSettingsStore.swift UIImage (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) and runShortcut(_:) Strand/System/MacActions.swift macOS-only; the strap-double-tap actions have no direct iOS analogue
    .windowStyle(.hiddenTitleBar) / .defaultSize window chrome Strand/App/StrandApp.swift Drop window modifiers; use a normal iOS scene

    Because the design system (StrandDesign) already bridges NSColor/UIColor behind #if canImport(AppKit) / #elseif canImport(UIKit), the palette, fonts, and most components carry over without edits.


Android (planned)

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.


Testing

# 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' test

Credits

NOOP builds on prior open-source reverse-engineering and interoperability work:

  • johnmiddleton12/my-whoop — WHOOP 4.0 BLE protocol; the WhoopProtocol and WhoopStore packages are adapted from this work.
  • b-nnett/goose — WHOOP 5.0 / MG BLE protocol (service family fd4b0001-…, 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.