# 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`](../DISCLAIMER.md) and [`ATTRIBUTION.md`](../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**; **Android ships as a full app** under `android/`, and **iOS is an experimental, build-from-source community port** (see [PR #42](../../../pull/42)). All 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: ```bash 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 ```bash cd /path/to/Strand xcodegen generate ``` ### 2. Build ```bash 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.app`** — `project.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`: ```xml com.apple.security.app-sandbox com.apple.security.device.bluetooth com.apple.security.files.user-selected.read-write ``` `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: ```bash 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: ```bash 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: ```swift 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 ```bash # 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: ```bash 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: ```bash cd Tools/Backfill swift run backfill ``` --- ## iOS (experimental community port — see [PR #42](../../../pull/42)) iOS is an **experimental, build-from-source community port** ([PR #42](../../../pull/42)) — an app target plus widgets, a Live Activity, and HealthKit that builds for the iOS simulator. It is **not officially maintained or distributed**: iOS has no anonymous distribution path (the App Store and TestFlight both require a real Apple Developer identity), which is fundamentally at odds with NOOP staying anonymous. There is no download — you build it yourself in Xcode. The port is feasible because all five packages already target `.iOS(.v16)`, so the **non-UI core compiles for iOS today**; the rest is the app-layer wiring captured in that PR and documented below. ### 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: ```yaml 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 restoration** — `BLEManager` 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 (shipped) Android ships as a **full, native client** — a separate Kotlin/Gradle module rather than a port of the Swift app. It lives under **`android/`** with its own `README`, and pre-built APKs (`NOOP-full.apk` plus a sample-data `NOOP-demo.apk`) are published in [Releases](../../../releases). 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 macOS app remains the reference implementation; the shared packages define the protocol, > storage, analytics, and import behavior every client matches. --- ## Testing ```bash # 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 community 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`](../ATTRIBUTION.md) for full detail.