Repository navigation
feat: decode WHOOP 5/MG raw 6-axis IMU offload buffer + activity features (#423) - #455
Merged
Merged
Conversation
The 1244-byte type-0x2F offload buffer carries a full second of raw inertial data: 100 accelerometer + 100 gyroscope samples, columnar i16 LE (ax/ay/az then gx/gy/gz), same convention as the 4.0 1917 variant and the layout dofek documents. accel 1/4096 g/LSB, gyro 2000/32768 (deg/s)/LSB. Whoop5RawImu.decode gates on the exact length + the two in-packet sample counts (=100). Pure/deterministic, no strap. VALIDATED by a test that decodes a REAL captured buffer (fw 50.40.1.0) and asserts the accelerometer forms a ~1g gravity shell (>=95/100 samples) with resting gyro small -- plus a synthetic-frame test pinning the exact offsets and scales. Note: the 5/MG LIVE raw-IMU stream is firmware-refused (cmd 106), but the OFFLOAD buffer carries full accel+gyro, so 100Hz 6-axis IMU IS obtainable via the historical path. Decode only; wiring into the store/HAR follows.
ImuFeatureExtractor turns a window of decoded 100Hz 6-axis samples (WhoopProtocol.Whoop5RawImu) into a compact activity-feature vector: accel AC energy (intensity), gyro energy (rotation), jerk RMS (impact), and gait cadence via band-limited autocorrelation of the accel-magnitude AC + its normalized strength. This is the real-IMU input for coarse sport / HAR classification -- the upgrade path the 1Hz-proxy WorkoutTypeClassifier (#414) was built to accept behind WorkoutTypeClassifying. Per the derived-signal rule, cadence is a feature (never a physiological gate) and the test recovers MULTIPLE distinct injected cadences (1.4/1.8/ 2.4/3.0 Hz), not one match, plus still/energy/gyro/end-to-end cases. Pure, no strap. On real captured data (23 min, fw 50.40.1.0) this same method found a 1.79 Hz = ~107 steps/min walking cadence -- which the 1Hz gravity stream physically cannot resolve.
ryanbr
added a commit
that referenced
this pull request
Jul 15, 2026
…ep-buffer log (#423) The #454 recorder banks the raw 1244-B/2140-B type-0x2F offload buffers, and #455 added a validated 6-axis IMU decoder — but nothing called it outside its own tests. Run Whoop5RawImu.decode + ImuFeatureExtractor on each captured 1244-B IMU buffer and inline the activity summary (cadence/energy/jerk/gyro) as an "imu" field on the existing JSONL line. This is the first caller of the decoder outside tests: it exercises it on real device captures and makes each line self-checking (raw hex vs decode) with no stored table, migration, or downstream gate. Instrumentation only, per the derived-signal rule; the 2140-B optical buffer stays raw-only (undecoded). decodedImuField is pure and non-throwing — a decode miss (optical buffer, or count fields != 100) omits the field and can never disturb the capture path.
ryanbr
added a commit
that referenced
this pull request
Jul 15, 2026
…ep-buffer log (#423) (#476) The #454 recorder banks the raw 1244-B/2140-B type-0x2F offload buffers, and #455 added a validated 6-axis IMU decoder — but nothing called it outside its own tests. Run Whoop5RawImu.decode + ImuFeatureExtractor on each captured 1244-B IMU buffer and inline the activity summary (cadence/energy/jerk/gyro) as an "imu" field on the existing JSONL line. This is the first caller of the decoder outside tests: it exercises it on real device captures and makes each line self-checking (raw hex vs decode) with no stored table, migration, or downstream gate. Instrumentation only, per the derived-signal rule; the 2140-B optical buffer stays raw-only (undecoded). decodedImuField is pure and non-throwing — a decode miss (optical buffer, or count fields != 100) omits the field and can never disturb the capture path.
2 of 3 tasks
ryanbr
pushed a commit
that referenced
this pull request
Jul 15, 2026
… IMU decode (#423) (#481) Ports the Swift #423 research cluster (upstream #454/#455/#476) to Kotlin so noop-tan reaches parity with the WhoopProtocol/StrandAnalytics/BLE Swift side: - Whoop5RawImu (com.noop.protocol): decode the 1244-byte 5/MG raw 6-axis IMU offload buffer — 100 Hz accel (1/4096 g) + gyro (2000/32768 deg/s), columnar i16 LE at the frame-absolute offsets 15/24/28/228/428/630/640/840/1040. Gated on length + both in-packet counts (=100), not the type byte. baseTs is Long (Swift Int = 64-bit; holds the full u32 without truncation). - ImuFeatureExtractor (com.noop.analytics): activity features (accel AC energy, gyro energy, jerk RMS, autocorrelation cadence + strength) over a window of decoded samples. Cadence is a FEATURE reported with its own strength, never a physiological gate (derived-signal rule). - PuffinDeepBufferLog (com.noop.ble) + WhoopBleClient wiring: durable append-only JSONL of the big (>= 1 KB) type-0x2F R22 deep buffers (1244/2140 B) for offline layout RE, in its own file the bulk capture never churns. The 1244-B IMU buffer carries its decoded activity summary inline. Gated on the existing capture toggle (PuffinExperiment.isCaptureEnabled); passive/read-only, no new setting. Byte-identical to the Swift decode/features (verified against the same synthetic vectors + a real 1244-byte buffer captured off a WHOOP 5.0 fw 50.40.1.0: baseTs 1784037165, ~1 g gravity shell). The inline-IMU JSON is value-parity (numbers may format differently than Swift's JSONEncoder — both valid JSON — since it's a research JSONL the analysis tool parses, not a stored .noopbak value). 14 new unit tests; full Full-debug suite green (2660 tests, 0 fail).
ryanbr
added a commit
that referenced
this pull request
Jul 17, 2026
…ller
Doc-only. The earlier wording ("decode first, consume later") implied a house pattern that
does not exist -- I asserted it without checking. Whoop5RawImu.kt shipped WITH its
PuffinDeepBufferLog caller in a single commit (091b3c6, #481), and the Swift side landed
decoder + ImuFeatureExtractor + caller together (#455). Neither ever sat callerless, which
makes this the protocol package's first callerless decoder.
The real reason is narrower and worth stating accurately: no parity-safe consumer exists yet.
v20 channel identity is unproven, and Swift deliberately keeps the 2140-B buffer raw-only in
its deep-buffer log, so wiring a consumer on Android alone would re-open the parity gap from
the other side. A consumer wants both platforms at once, once #423 settles identity.
Tested: ./gradlew testFullDebugUnitTest --tests "com.noop.protocol.*" --no-build-cache ->
168/168 unchanged.
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
…ures (ryanbr#423) (ryanbr#455) * feat(protocol): decode WHOOP 5/MG raw 6-axis IMU offload buffer (ryanbr#423) The 1244-byte type-0x2F offload buffer carries a full second of raw inertial data: 100 accelerometer + 100 gyroscope samples, columnar i16 LE (ax/ay/az then gx/gy/gz), same convention as the 4.0 1917 variant and the layout dofek documents. accel 1/4096 g/LSB, gyro 2000/32768 (deg/s)/LSB. Whoop5RawImu.decode gates on the exact length + the two in-packet sample counts (=100). Pure/deterministic, no strap. VALIDATED by a test that decodes a REAL captured buffer (fw 50.40.1.0) and asserts the accelerometer forms a ~1g gravity shell (>=95/100 samples) with resting gyro small -- plus a synthetic-frame test pinning the exact offsets and scales. Note: the 5/MG LIVE raw-IMU stream is firmware-refused (cmd 106), but the OFFLOAD buffer carries full accel+gyro, so 100Hz 6-axis IMU IS obtainable via the historical path. Decode only; wiring into the store/HAR follows. * feat(analytics): IMU activity features from decoded 5/MG raw IMU (ryanbr#423) ImuFeatureExtractor turns a window of decoded 100Hz 6-axis samples (WhoopProtocol.Whoop5RawImu) into a compact activity-feature vector: accel AC energy (intensity), gyro energy (rotation), jerk RMS (impact), and gait cadence via band-limited autocorrelation of the accel-magnitude AC + its normalized strength. This is the real-IMU input for coarse sport / HAR classification -- the upgrade path the 1Hz-proxy WorkoutTypeClassifier (ryanbr#414) was built to accept behind WorkoutTypeClassifying. Per the derived-signal rule, cadence is a feature (never a physiological gate) and the test recovers MULTIPLE distinct injected cadences (1.4/1.8/ 2.4/3.0 Hz), not one match, plus still/energy/gyro/end-to-end cases. Pure, no strap. On real captured data (23 min, fw 50.40.1.0) this same method found a 1.79 Hz = ~107 steps/min walking cadence -- which the 1Hz gravity stream physically cannot resolve.
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
…ep-buffer log (ryanbr#423) (ryanbr#476) The ryanbr#454 recorder banks the raw 1244-B/2140-B type-0x2F offload buffers, and ryanbr#455 added a validated 6-axis IMU decoder — but nothing called it outside its own tests. Run Whoop5RawImu.decode + ImuFeatureExtractor on each captured 1244-B IMU buffer and inline the activity summary (cadence/energy/jerk/gyro) as an "imu" field on the existing JSONL line. This is the first caller of the decoder outside tests: it exercises it on real device captures and makes each line self-checking (raw hex vs decode) with no stored table, migration, or downstream gate. Instrumentation only, per the derived-signal rule; the 2140-B optical buffer stays raw-only (undecoded). decodedImuField is pure and non-throwing — a decode miss (optical buffer, or count fields != 100) omits the field and can never disturb the capture path.
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
… IMU decode (ryanbr#423) (ryanbr#481) Ports the Swift ryanbr#423 research cluster (upstream ryanbr#454/ryanbr#455/ryanbr#476) to Kotlin so noop-tan reaches parity with the WhoopProtocol/StrandAnalytics/BLE Swift side: - Whoop5RawImu (com.noop.protocol): decode the 1244-byte 5/MG raw 6-axis IMU offload buffer — 100 Hz accel (1/4096 g) + gyro (2000/32768 deg/s), columnar i16 LE at the frame-absolute offsets 15/24/28/228/428/630/640/840/1040. Gated on length + both in-packet counts (=100), not the type byte. baseTs is Long (Swift Int = 64-bit; holds the full u32 without truncation). - ImuFeatureExtractor (com.noop.analytics): activity features (accel AC energy, gyro energy, jerk RMS, autocorrelation cadence + strength) over a window of decoded samples. Cadence is a FEATURE reported with its own strength, never a physiological gate (derived-signal rule). - PuffinDeepBufferLog (com.noop.ble) + WhoopBleClient wiring: durable append-only JSONL of the big (>= 1 KB) type-0x2F R22 deep buffers (1244/2140 B) for offline layout RE, in its own file the bulk capture never churns. The 1244-B IMU buffer carries its decoded activity summary inline. Gated on the existing capture toggle (PuffinExperiment.isCaptureEnabled); passive/read-only, no new setting. Byte-identical to the Swift decode/features (verified against the same synthetic vectors + a real 1244-byte buffer captured off a WHOOP 5.0 fw 50.40.1.0: baseTs 1784037165, ~1 g gravity shell). The inline-IMU JSON is value-parity (numbers may format differently than Swift's JSONEncoder — both valid JSON — since it's a research JSONL the analysis tool parses, not a stored .noopbak value). 14 new unit tests; full Full-debug suite green (2660 tests, 0 fail).
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…ures (ryanbr#423) (ryanbr#455) * feat(protocol): decode WHOOP 5/MG raw 6-axis IMU offload buffer (ryanbr#423) The 1244-byte type-0x2F offload buffer carries a full second of raw inertial data: 100 accelerometer + 100 gyroscope samples, columnar i16 LE (ax/ay/az then gx/gy/gz), same convention as the 4.0 1917 variant and the layout dofek documents. accel 1/4096 g/LSB, gyro 2000/32768 (deg/s)/LSB. Whoop5RawImu.decode gates on the exact length + the two in-packet sample counts (=100). Pure/deterministic, no strap. VALIDATED by a test that decodes a REAL captured buffer (fw 50.40.1.0) and asserts the accelerometer forms a ~1g gravity shell (>=95/100 samples) with resting gyro small -- plus a synthetic-frame test pinning the exact offsets and scales. Note: the 5/MG LIVE raw-IMU stream is firmware-refused (cmd 106), but the OFFLOAD buffer carries full accel+gyro, so 100Hz 6-axis IMU IS obtainable via the historical path. Decode only; wiring into the store/HAR follows. * feat(analytics): IMU activity features from decoded 5/MG raw IMU (ryanbr#423) ImuFeatureExtractor turns a window of decoded 100Hz 6-axis samples (WhoopProtocol.Whoop5RawImu) into a compact activity-feature vector: accel AC energy (intensity), gyro energy (rotation), jerk RMS (impact), and gait cadence via band-limited autocorrelation of the accel-magnitude AC + its normalized strength. This is the real-IMU input for coarse sport / HAR classification -- the upgrade path the 1Hz-proxy WorkoutTypeClassifier (ryanbr#414) was built to accept behind WorkoutTypeClassifying. Per the derived-signal rule, cadence is a feature (never a physiological gate) and the test recovers MULTIPLE distinct injected cadences (1.4/1.8/ 2.4/3.0 Hz), not one match, plus still/energy/gyro/end-to-end cases. Pure, no strap. On real captured data (23 min, fw 50.40.1.0) this same method found a 1.79 Hz = ~107 steps/min walking cadence -- which the 1Hz gravity stream physically cannot resolve.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…ep-buffer log (ryanbr#423) (ryanbr#476) The ryanbr#454 recorder banks the raw 1244-B/2140-B type-0x2F offload buffers, and ryanbr#455 added a validated 6-axis IMU decoder — but nothing called it outside its own tests. Run Whoop5RawImu.decode + ImuFeatureExtractor on each captured 1244-B IMU buffer and inline the activity summary (cadence/energy/jerk/gyro) as an "imu" field on the existing JSONL line. This is the first caller of the decoder outside tests: it exercises it on real device captures and makes each line self-checking (raw hex vs decode) with no stored table, migration, or downstream gate. Instrumentation only, per the derived-signal rule; the 2140-B optical buffer stays raw-only (undecoded). decodedImuField is pure and non-throwing — a decode miss (optical buffer, or count fields != 100) omits the field and can never disturb the capture path.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
… IMU decode (ryanbr#423) (ryanbr#481) Ports the Swift ryanbr#423 research cluster (upstream ryanbr#454/ryanbr#455/ryanbr#476) to Kotlin so noop-tan reaches parity with the WhoopProtocol/StrandAnalytics/BLE Swift side: - Whoop5RawImu (com.noop.protocol): decode the 1244-byte 5/MG raw 6-axis IMU offload buffer — 100 Hz accel (1/4096 g) + gyro (2000/32768 deg/s), columnar i16 LE at the frame-absolute offsets 15/24/28/228/428/630/640/840/1040. Gated on length + both in-packet counts (=100), not the type byte. baseTs is Long (Swift Int = 64-bit; holds the full u32 without truncation). - ImuFeatureExtractor (com.noop.analytics): activity features (accel AC energy, gyro energy, jerk RMS, autocorrelation cadence + strength) over a window of decoded samples. Cadence is a FEATURE reported with its own strength, never a physiological gate (derived-signal rule). - PuffinDeepBufferLog (com.noop.ble) + WhoopBleClient wiring: durable append-only JSONL of the big (>= 1 KB) type-0x2F R22 deep buffers (1244/2140 B) for offline layout RE, in its own file the bulk capture never churns. The 1244-B IMU buffer carries its decoded activity summary inline. Gated on the existing capture toggle (PuffinExperiment.isCaptureEnabled); passive/read-only, no new setting. Byte-identical to the Swift decode/features (verified against the same synthetic vectors + a real 1244-byte buffer captured off a WHOOP 5.0 fw 50.40.1.0: baseTs 1784037165, ~1 g gravity shell). The inline-IMU JSON is value-parity (numbers may format differently than Swift's JSONEncoder — both valid JSON — since it's a research JSONL the analysis tool parses, not a stored .noopbak value). 14 new unit tests; full Full-debug suite green (2660 tests, 0 fail).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Decode the WHOOP 5.0/MG raw 6-axis IMU offload buffer and extract activity features from it. Pure, tested, no strap, no decode-gating of any existing score. Follows the capture recorder in #454; coordinates with #423.
WhoopProtocol.Whoop5RawImu— the 1244-byte type-0x2F offload buffer → 100 Hz 3-axis accel (g) + 3-axis gyro (°/s).StrandAnalytics.ImuFeatureExtractor— a window of samples →{accelEnergy, gyroEnergy, jerkRms, cadenceHz, cadenceStrength}, the input a coarse sport / HAR classifier needs (the real-IMU upgrade path behindWorkoutTypeClassifying).Why
The 5/MG's live raw-IMU stream is firmware-refused (
TOGGLE_IMU_MODE/106 acks but never streams, #423), so this looked like a dead end. But the offload burst carries a 1244-byte buffer that is a full second of raw inertial data — including gyro. So 100 Hz 6-axis IMU is obtainable on the 5.0, via the historical path. At 100 Hz it resolves gait cadence, jerk/impact, and rotational energy the 1 Hz gravity vector physically cannot (a 1.8 Hz step rate is above the 1 Hz stream's Nyquist limit).Layout (validated)
Reference: Asherlc/dofek
docs/whoop-ble-protocol.mddocuments this raw packet; same columnar convention as the 4.01917variant.Verification
swift test—Whoop5RawImuTests+ImuFeatureExtractorTests(pure packages, run in CI'sswift-packages).Notes