v26: decode the per-record metadata, including the channel index at frame-abs 25 - #24
v26: decode the per-record metadata, including the channel index at frame-abs 25#24sunny-noop wants to merge 1 commit into
Conversation
…rame-abs 25 Adds subChannel/segmentId/gain/flags; segmentId corrects a u16 that was being read as a byte.
|
Warning Review limit reached
Next review available in: 36 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
v26: decode the per-record metadata the record already carries
Gen5PpgWaveformcurrently surfaces four fields. The record carries more, and one of the bytes itdrops is the one that separates the interleaved optical channels. Without it a burst is several
channels mixed together, which is why anything downstream of the v26 waveform is hard to make work.
Scope: gen5 v26 only. Corpus is 86,859 v26 records from two bands worn by two different people
(18,023 and 68,836). Every record observed has an inner length of 76.
1. Frame-abs 25 is an acquisition-channel index (0..7)
This is the field the current
burstIndexdoc goes looking for and correctly rules out ("NOT achannel/LED id, ranges past 26"). It is a different byte.
burstIndexat frame-abs 21 does indeed run1..72 and 1..84 on the two bands, so that note stands. Frame-abs 25 is the channel index.
Distribution:
Three things say "channel" rather than "another counter":
It multiplexes inside a single burst. Splitting the stream into bursts on a gap of more than 5 s,
53% of bursts on band A and 59% on band B contain more than one value of this byte, in short runs. A
time or sequence counter would not alternate and come back.
Each value runs its own gain. The gain bytes at frame-abs 79 and 80 have a characteristic setting
per channel. On band A, channels 3 and 4 sit mostly at setting 80 while channels 1 and 2 sit at 160,
and channel 7 uses much higher index values than the rest. That is a per-channel front-end
configuration, not a label.
Each value carries its own pulse. Taking runs of 8 or more consecutive records with the same
channel index and treating the concatenated samples as 24 Hz, the dominant spectral peak between 0.5
and 4 Hz falls in the cardiac band (0.7 to 3.5 Hz) for:
So every channel is a real independent optical measurement, not padding or a duplicate.
What I am not claiming is which wavelength each index is. I can show the byte separates distinct
acquisition channels; I cannot show from these captures that channel 2 is red and channel 3 is IR, so
the field is exposed as an index with no wavelength mapping.
Values outside 0..7 appear on about 0.2% of records (0xFD to 0xFF). They still look like a pulse, so I
have not called them invalid, but they are not a channel index either.
subChannelKnowngates to 0..7and returns null otherwise, following the
activityClassKnownpattern.2. Frame-abs 19 is a u16, not a byte
rawByte19readsinner[11]. The high byteinner[12]is nonzero on 99.17% of band A records and98.95% of band B records, so the current read discards most of the value. The fixture already in
this repo shows it: the u16 is 18350, the byte read gives 174, and the discarded high byte is 71.
Read as a u16 it has an exact closed form. It is an integer
kin 0..99 packed as a Q15 fraction:That holds for 86,859 of 86,859 records, across 99 distinct values on one band and 100 on the
other. The value is constant across the records of a burst (91% to 93% of bursts under the crude
5-second split above, so probably all of them under a better burst definition).
segmentIndexreturnsk. Whatkcounts is not established and I have not claimed it.rawByte19is kept and deprecated rather than removed.
3. The block after the waveform
Frame-abs 75 to 82 carries a f32 and four bytes that nothing currently reads:
signalMetricf32flagBis set, on both bands. Reads as a per-record quality or residual where low means clean. Scale unpinned.gainSetting/gainIndexflagA/flagBflagBis set on 57% to 75% of records. Meaning otherwise unestablished.frontEndMetaRawu16signalMetric. Exposed raw so the gap is documented rather than silently skipped.I have deliberately not named these beyond what the numbers support.
4. Short records
This block sits after the waveform, past the existing 67-byte floor, so a record with fewer samples
could carry a waveform and no metadata. Rather than raise
kGen5V26MinInnerLenand start rejectingrecords that currently decode, the trailing fields are nullable and gated on
kGen5V26MinInnerLenWithMeta(75). A short record decodes its waveform, its channel index and itssegment id, and returns null for the rest instead of a stand-in value. There is a test for it.
Every real record I have is 76 bytes, so this path is defensive rather than observed.
What changed
segmentId(u16) withsegmentIndex,subChannelwith a gatedsubChannelKnown,frontEndMetaRaw,signalMetric,gainSetting,gainIndex,flagA,flagB, andkGen5V26MinInnerLenWithMeta.rawByte19is deprecated, not removed. The waveform decode is untouched, so the frozen parity oracleis unaffected.
Note that the CRC32 at frame-abs 84 is the standard gen5 frame trailer that
parseFramealreadychecks, not a v26 field. I checked before writing this up in case it needed exposing. It does not.
Verification
dart analyzeclean,dart test164 passed (5 new, plus the usual 4 skips for the capture replay setthat lives beside the repo). The new tests cover the u16 versus byte read, the Q15 packing, the
metadata block against the existing real fixture, the out-of-range channel gate, and the short-record
path.