-
Notifications
You must be signed in to change notification settings - Fork 649
Contributing
Welcome! NOOP is built and maintained by community contributors. This page covers how to help, how to build locally, the safety rules that govern the codebase, and the licensing terms.
Not affiliated with WHOOP, and not a medical device. NOOP reads your own data off your own device; it contains no WHOOP code, firmware, or assets and performs no DRM circumvention. Every derived metric is an approximation and is not clinically validated. See Privacy and Security and the Disclaimer and Legal.
A few principles run through the whole codebase. Internalize them before opening a PR.
-
Offline by design. There is no server, no telemetry, no account, no network call. A change that phones home — for any reason — does not belong here. Strap data, imports, and computed metrics live in a local SQLite database and never leave the device.
-
Interoperability, not impersonation. NOOP talks to a strap the user already owns. It does not log into a WHOOP account, bypass a paywall, or ship WHOOP's proprietary code/firmware/assets/logos. Keep all WHOOP references nominative (used only to name the hardware).
-
Never destructive on the wire. The strap is real hardware on someone's wrist. The app only sends a curated, reversible command set. See The BLE safety contract below.
-
Transparent math. Analytics are approximations of published methods, documented file by file. No black boxes, no claims of clinical accuracy.
-
Credit upstream. The protocol work is built on prior community reverse-engineering —
johnmiddleton12/my-whoop(WHOOP 4.0) andb-nnett/goose(WHOOP 5.0). Preserve those credits in code comments and documentation.
Not all contributions are code. Here's what moves NOOP forward:
| Type | How |
|---|---|
| Report bugs | File a GitHub issue with reproduction steps, hardware info, and logs (if applicable). |
| Share strap logs | Captured BLE frames help with protocol RE and debugging. See tools/linux-capture/README.md for how to capture on Linux. |
| Test on real hardware | Test PRs and new features on your own strap. Report what works and what doesn't. |
| Code contributions | Protocol decoding, analytics, storage, UI, or platform support. Read the Development section below first. |
| Documentation | Improve README, docs, code comments, or contribute to this wiki. Clear writing is code that scales. |
| Star the repo | Visibility helps the project. It only takes a click. |
| Donations | Optional. See Donations. |
| Tool | Notes |
|---|---|
| macOS 13+ | Deployment target. |
| Xcode 15+ (Swift 5.9 toolchain) | Provides xcodebuild and the macOS SDK. |
| XcodeGen 2.45+ | Generates Strand.xcodeproj from project.yml. Install via 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.
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.
Strand/
├── project.yml # XcodeGen project definition — source of truth for macOS
├── Strand.xcodeproj/ # Generated — do NOT hand-edit (gitignored)
├── 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
│ ├── Data/ # Repository, importers, MetricCatalog, settings
│ ├── Screens/ # SwiftUI screens (Today, Sleep, Trends, etc.)
│ ├── MenuBar/ # Menu bar content (glanceable live HR)
│ ├── System/ # MacActions (lock screen, Shortcuts), ProjectInfo
│ └── Resources/ # Info.plist, Strand.entitlements, 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
├── tools/
│ └── linux-capture/ # Linux capture workbench (Python/bleak + whoop-decode)
├── Fixtures/ # Sample WHOOP export for tests
└── android/ # Android client — full shipped app (Kotlin/Gradle)
| If your change is about… | It belongs in… | Notes |
|---|---|---|
| Decoding strap bytes, CRC, framing, packet/event types | Packages/WhoopProtocol |
Platform-pure — no CoreBluetooth. Runs in tests/CLI unchanged. |
| Persisting decoded data, migrations, caches, reads | Packages/WhoopStore |
GRDB/SQLite only. |
| Computing recovery / strain / HRV / sleep / correlations | Packages/StrandAnalytics |
Pure, database-free analyzers. |
| Parsing WHOOP CSV or Apple Health exports | Packages/StrandImport |
Header-name-driven CSV; streaming SAX XML. |
| Colors, fonts, motion, cards, charts | Packages/StrandDesign |
No external UI deps; bridges AppKit/UIKit. |
| CoreBluetooth, bonding, offload, live state |
Strand/BLE, Strand/Collect
|
macOS app layer. |
| A screen, sidebar item, menu-bar UI, automation |
Strand/Screens, Strand/App, Strand/System
|
App layer. |
| Capturing strap frames on Linux | tools/linux-capture |
Python/bleak capture. |
Rule of thumb: the more "wire-level" or "math-level" a change is, the deeper into Packages/ it should live, and the more it should be covered by unit tests that run without an app, a strap, or CoreBluetooth.
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. Framework-specific code must be guarded:
#if canImport(AppKit)
let ns = NSColor(self).usingColorSpace(.sRGB) ?? NSColor(self)
#elseif canImport(UIKit)
let ui = UIColor(self)
#endifDo not add import AppKit / import UIKit / import CoreBluetooth to any file under Packages/ — that breaks the cross-platform contract. CoreBluetooth lives only in the macOS app's Strand/BLE.
The Xcode project is generated, not committed. project.yml is the source of truth.
1. Generate the Xcode project
cd /path/to/Strand
xcodegen generateRe-run this whenever you add/remove source files or edit project.yml.
2. Build
xcodebuild \
-project Strand.xcodeproj \
-scheme Strand \
-destination 'platform=macOS' \
CODE_SIGNING_ALLOWED=NO \
buildNotes:
- The scheme is
Strand; the built product isNOOP.app—project.ymlsetsPRODUCT_NAME: NOOP, bundle idcom.noopapp.noop. -
CODE_SIGNING_ALLOWED=NOskips signing for a fast local loop. - Swift Package Manager resolves GRDB.swift (SQLite) and ZIPFoundation (export unzip) automatically on first build.
3. The ad-hoc-signed NOOP.app (for running)
The app is sandboxed and requests Bluetooth + user-selected-file access. To produce a runnable bundle, build without disabling signing:
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 to your Applications folder and launch. On first run, macOS prompts for Bluetooth permission.
4. Development 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
# run full app tests:
xcodebuild -project Strand.xcodeproj -scheme Strand -destination 'platform=macOS' testYou can also open the generated project interactively:
open Strand.xcodeproj5. Pairing & BLE on macOS
NOOP connects over CoreBluetooth. The central manager is created on the main queue 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.
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 backfillMost contributions live inside one package. Build and test it in isolation — it's far faster than the whole app and needs no strap:
cd Packages/WhoopProtocol && swift build && swift test
cd Packages/WhoopStore && swift build && swift test
cd Packages/StrandAnalytics && swift build && swift test
cd Packages/StrandImport && swift build && swift test
cd Packages/StrandDesign && swift build && swift testThe pure packages build and test on Linux with the standard Swift toolchain (no Apple frameworks). WhoopProtocol also produces a whoop-decode CLI:
cd Packages/WhoopProtocol
swift build && swift test # decoder + tests on Linux
swift build --product whoop-decode # the CLI → .build/debug/whoop-decode
cd ../../tools/linux-capture
python3 -m unittest -v # framing/reassembly testsCapturing from a real strap on Linux is documented in tools/linux-capture/README.md.
Android ships as a full, native client — a separate Kotlin/Gradle module under android/ with its own README and build instructions. Pre-built APKs are published in Releases.
Toolchain: JDK 17, Android Studio (current stable with Android SDK), Gradle. Open the android/ directory in Android Studio, let Gradle sync, and run on a device with Bluetooth.
- Swift, four-space indent, no trailing whitespace. Match the surrounding file.
-
Public API is intentional.
publiconly what a consumer package or the app actually needs. Types crossing concurrency boundaries areSendablewhere it makes sense. - Document the "why", and cite sources. Protocol and analytics code carries comments explaining where a fact came from (e.g., "Ported from Goose reverse-engineering"; "Task Force 1996 HRV"; "Karvonen %HRR"). Preserve and extend these citations — they keep the math transparent and the protocol auditable.
-
Pure where possible.
WhoopProtocoldecode functions,StrandAnalyticsanalyzers, andFrameRouterare deliberately free of side effects and frameworks so they're unit-testable. Keep new logic in that pure style and let the app layer do the I/O. -
@MainActorfor UI-touching state.FrameRouterand live-state types are main-actor isolated;CBCentralManageris created on.mainso delegate callbacks land on the main actor. -
No anonymous magic. Reach for an existing constant/enum (
NoopMetrics,StrandPalette,WhoopCommand,MetricCatalog) before introducing a literal. - Validate before you trust. Any data coming off the wire is gated on its checksum and range-checked before it can drive state. New inbound paths follow the same pattern.
The strap is real hardware on someone's wrist. The Bluetooth path is the highest-stakes code in the repo, and it has hard rules. A PR that violates any of them will not be merged.
The app's outbound command set lives in Strand/BLE/Commands.swift as WhoopCommand. It is intentionally a curated subset of the strap's command space. Every command currently in the enum is safe and reversible — toggle realtime HR, read clock / battery / version / data range, run/stop a haptic pattern, arm/read/cancel the firmware alarm, start/stop raw data.
Do not add reboot, firmware/DFU, ship-mode/power-cycle, force-trim, fuel-gauge reset, or any command that can brick, wipe, or permanently alter the device. If you believe a non-trivial command is genuinely needed, open an issue first, justify why it's reversible, and document its payload and on-device verification before any code.
Frames are only acted on after both CRCs pass. Outbound frames are built with correct CRCs; inbound frames are rejected if their checksum fails.
-
Outbound:
WhoopCommand.frame(seq:payload:)builds the frame with correct CRC8 (for WHOOP 4) or CRC16-Modbus (for WHOOP 5) and zlib CRC32. -
Inbound:
FrameRouter.handle(frame:)rejects any frame whosecrcOK == falsebefore it can touchLiveState. Bad bytes never drive state. New inbound paths must do the same.
Never short-circuit a CRC check "to make a capture work". If a real frame fails CRC, the bug is in the framing/decoding, not in the check.
The connect/bond/offload state machine in Strand/BLE/BLEManager.swift is load-bearing and was hardened against real failure modes. Treat it as stable infrastructure:
-
Don't reorder the connect handshake. Offload is deliberately gated on
connectHandshakeDone;SET_CLOCKmust precede arming the firmware alarm so the strap RTC is UTC-correct. -
Don't
ENTERhigh-frequency sync. The app sendsexitHighFreqSyncdefensively on connect to release straps parked there by older builds. -
Prefer
.withoutResponsewrites. Use.withResponseonly where an ack is genuinely required (e.g.,historicalDataResult). - Verify on real hardware. Anything that changes what bytes go out, or when, must be tested against an actual strap and the result noted in the PR.
The decode core (
WhoopProtocol) and the router (FrameRouter) are pure and unit-tested, so you can iterate on parsing and routing logic withswift testand captured frames without a strap. Reserve on-device testing for the connection/command behavior that genuinely needs it.
StrandDesign is the single source of visual truth. Every screen composes only its tokens and components. Do not hardcode colors, sizes, fonts, or invent ad-hoc cards.
Never write a raw hex value or a system color in a screen. Pull from Packages/StrandDesign/Sources/StrandDesign/Palette.swift. Semantic tokens exist for surfaces (surfaceBase / surfaceRaised / surfaceOverlay / surfaceInset), text (textPrimary / textSecondary / textTertiary), borders (hairline / hairlineStrong), status colors (statusPositive / statusWarning / statusCritical), and the recovery/strain gradients. Use the sampling helpers (recoveryColor, strainColor, sleepStageColor, hrZoneColor) rather than picking a stop by hand.
From Typography.swift. Use the named scale (title1, title2, headline, body, caption, overline, mono). All live/numeric values use tabular digits — use StrandFont.number(_:), bodyNumber, captionNumber, or display(_:). For ALL-CAPS overline labels, use the Text.strandOverline() helper.
Build screens from shared pieces: NoopCard, StatTile, ChartCard / ChartFooter, SectionHeader, InsightCard, SegmentedPillControl, SourceBadge, RecoveryRing, StrainGauge, Hypnogram, Sparkline, TrendChart, YearHeatStrip, StatePill.
Spacing and sizing come from NoopMetrics (cardRadius, cardPadding, gap, sectionGap, screenPadding, tileHeight, chartHeight) and animation from StrandMotion (interactive, gentle, hero).
If you find yourself writing a one-off card, gradient, font size, or animation in a screen, stop — either it already exists in StrandDesign, or it should be added there (with a #Preview) and then used. Screens stay thin; the system stays canonical.
A "metric" is a named daily/series value that flows from an importer or analyzer into SQLite and out to the UI.
-
Write the series. An importer (
Packages/StrandImport) or analyzer (Packages/StrandAnalytics) produces points; they're persisted viaWhoopStore.upsertMetricSeries(_:deviceId:)into themetricSeriestable. The serieskeymust match exactly what the catalog expects. -
Register it in the catalog. Add a
MetricDescriptorrow inStrand/Data/MetricCatalog.swift:d("resp_rate", "Respiratory Rate", "Recovery", "rpm", "my-whoop", "lungs", 1, nil), // key title category unit source sf-symbol decimals higherIsBetter
-
key— the exactmetricSerieskey the importer/analyzer writes. -
category— one ofMetricCatalog.categories(Heart,Recovery,Sleep,Strain,Health). -
source—"my-whoop"(strap/CSV) or"apple-health"; drives theSourceBadge. -
higherIsBetter—true/false/nil; controls delta tinting.
-
- That's it for the UI. Metric Explorer and Compare are built from the catalog, so a correctly registered metric with data behind it appears automatically.
- Add a test. Cover the parse/compute in that package's test suite.
Verify the key in three places before you push: what the importer/analyzer writes, the
MetricCatalogkey, and any SQLWHERE key = …. Mismatched keys are a known class of bug here.
-
Build it from
StrandDesign. ComposeNoopCard,StatTile,ChartCard, etc.; pull every color/font/size fromStrandPalette/StrandFont/NoopMetrics. Use the sharedScreenScaffoldfor standard screen chrome. -
Register it in the sidebar.
Strand/App/RootView.swiftdrives navigation from theNavItemenum:- Add a
casetoNavItem. - Add an SF Symbol in the
iconswitch. - Add the
caseto thedetailview-builder switch.
- Add a
-
Keep state where it belongs. Read through
AppModel/Repository; don't reach into CoreBluetooth or SQLite directly from a view. - Optional features default OFF. Anything that takes a Mac action, fires a notification, or automates behavior is opt-in and toggleable.
Only after re-reading The BLE safety contract.
- Confirm it is safe and reversible. If it can brick, wipe, reflash, or permanently alter the strap, it does not go in. No exceptions.
-
Add the case to
WhoopCommandinStrand/BLE/Commands.swiftwith its on-wire raw value, alabel, and a comment documenting the payload, what it does, why it's safe/reversible, and how it was verified on-device. -
Add a payload builder if needed (e.g.,
setAlarmPayload(epochSec:)), keeping the byte layout documented. -
Send it through the existing path —
BLEManager.send(_:payload:writeType:)— which frames the command (correct CRC) and writes to the command characteristic. - Verify on a real strap and record the result in the PR.
Schema lives in Packages/WhoopStore/Sources/WhoopStore/Database.swift as a versioned GRDB DatabaseMigrator (currently through v9).
-
Never edit an existing migration. They've already run on users' databases. Add a new
migrator.registerMigration("vN") { db in … }block. - The early migrations create the durable decoded-stream tables (
hrSample,rrInterval,spo2Sample,skinTempSample,respSample) keyed by(deviceId, ts); later ones add metric caches (sleepSession,dailyMetric,metricSeries). Follow the same shape and naming. - Add a
MigrationTestscase proving the migration applies cleanly on top of the prior version.
-
Each package owns its tests under
Packages/<Name>/Tests/…; run them withswift test. Coverage already includes framing/CRC parity, reassembly, schema, stream decode, store insert/read/migration/prune, the analyzers (HRV, recovery, strain, sleep, correlation, baselines), and the CSV / Apple Health importers. -
Fixtures/holds a sample WHOOP export for the import tests. -
Prefer pure tests. Because
WhoopProtocol,StrandAnalytics, andFrameRouterare framework-free, you can cover new decode/routing/math with captured frames and fixtures rather than requiring a strap. -
StrandTestsis the macOS app integration suite (run viaxcodebuild … test).
-
Generated artifacts stay out of git.
Strand.xcodeproj/,build/,.build/,*.app, and DerivedData are gitignored; commitproject.yml, not the generated project. - One concern per PR. Keep a protocol change, a schema migration, and a UI change in separate PRs where practical.
-
Show your verification. For anything on the BLE path, state what you tested on real hardware. For analytics, cite the method and add a test. For UI, confirm it uses only
StrandDesigntokens. -
Anonymous, project-voice. Documentation and comments are written in neutral, third-person project voice. Keep upstream credits (
my-whoop,goose,GRDB.swift,ZIPFoundation) intact. - No proprietary material. Don't add WHOOP firmware, decompiled app code, logos, or assets. Keep contributions to clean-room interoperability with hardware the user owns.
- Licensing. By opening a pull request you agree your contribution is licensed under the same PolyForm Noncommercial License 1.0.0 as the rest of NOOP. Forks and personal, non-commercial use are welcome under those terms.
NOOP is source-available under the PolyForm Noncommercial License 1.0.0:
- Free for personal and other non-commercial use.
- You may read, run, fork, and contribute.
- Commercial use is not granted by this license.
- By opening a pull request you agree your contribution is licensed under the same terms.
The license covers NOOP's own original code and docs. Protocol facts (frame layouts, command numbers, byte offsets) are uncopyrightable and free to reuse. Bundled dependencies keep their own licenses (GRDB.swift and ZIPFoundation are MIT — see NOTICE).
NOOP is not affiliated with, endorsed by, or connected to WHOOP, Inc. All references to "WHOOP" are nominative — used only to identify the third-party hardware NOOP interoperates with. See Disclaimer and Legal for full detail.
NOOP's logic already lives in cross-platform packages, so most platform work is app-layer wiring. Today the macOS app is the reference implementation and Android ships as a full app. The items below are planned, experimental, or deferred.
-
Windows app (planned). A native Windows BLE stack + UI re-implementation that matches the shared packages' behavior. The protocol facts in
WhoopProtocol/Resources/whoop_protocol.jsonand the framing/CRC rules are language-agnostic, so the wire behavior is portable. -
Android (shipped). A full native Kotlin/Gradle client lives under
android/, re-implementing the same wire protocol against Android's BLE stack. It pairs, offloads, persists and scores on-device, and imports WHOOP / Apple Health / Health Connect. Pre-built APKs are in Releases. -
iOS (experimental community port). An experimental, build-from-source port in PR #42 — app target + widgets + Live Activity + HealthKit that builds for the iOS simulator. It is build-it-yourself only, 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 at odds with NOOP staying anonymous. Every package already declares
.iOS(.v16)and guards UI-framework code with#if canImport(UIKit) / AppKit, so the non-UI core compiles for iOS today.
These are scoped but intentionally not built yet, so contributors know the direction before investing time:
- Live PPG scope. Surface the strap's raw optical (PPG) stream as a live waveform/diagnostic view. The raw-data plumbing exists, but a stable, useful live scope is deferred.
- Steps via IMU. Derive step count on-device from the strap's accelerometer instead of relying on imported Apple Health.
- Notification-watcher helper. A small, opt-in helper to mirror selected macOS notifications to a haptic cue on the strap. Strictly local, off by default, and bounded.
- Local AI coach. An on-device, offline assistant that reasons over your own series to produce plain-language guidance. Hard requirement: it must stay local and offline — no cloud inference, no data leaving the device.
Roadmap items don't change the ground rules. Everything above still holds: offline-only, no destructive BLE commands, CRC-gated, design-system-only UI, transparent and clearly-non-clinical math, and credit to the upstream reverse-engineering work.
Questions, feedback, or bugs? Email thenoopapp@gmail.com or file a GitHub issue.
NOOP is an independent, unofficial, non-commercial interoperability project, not affiliated with, endorsed by, or connected to WHOOP, Inc. See Disclaimer and Legal for full notice.
NOOP is an independent, unofficial, non-commercial interoperability project — not affiliated with, endorsed by, or sponsored by WHOOP, Inc. "WHOOP" is a trademark of WHOOP, Inc., used nominatively. Works only with a device you own; not a medical device; every metric is an approximation, not medical advice. · Privacy and Security · Donations · Releases
Get started
Tutorials
- Tracking a Workout
- Recovery, Strain & Readiness
- Automations
- Breathe & Intervals
- Importing History
- AI Coach
- Widget & Notifications
- Reading Your Sleep
- Explore & Compare
Reference
Project