DEV-793 Robust slow-sensor gap window: accumulated median + symmetric limits (review follow-up for #285) - #289
Merged
jyong15 merged 2 commits intoAug 21, 2026
Conversation
…n + symmetric limits Review follow-up for #285. The window that decides whether a slow-sensor (VD6283 light / MLX90632 skin temp) block boundary is continuous was re-derived on every payload from the same handful of inter-block spacings it then had to judge, and its fast edge came from a single extremum. - Accumulate the measured per-sample periods per sensor across the payloads of a parse run (UtilCsvSplitting.SLOW_SENSOR_OBSERVED_PERIODS_PER_SENSOR) instead of replacing the estimate with the latest payload's values. A payload only carries 2-3 slow-sensor blocks, so a per-payload window absorbed a dropped block's 2x spacing into its own centre and never reported it - while flagging the healthy boundary back to the previous payload instead. Against a history spanning hundreds of payloads a single 2x outlier barely moves the median. The history is bounded (SLOW_SENSOR_PERIOD_HISTORY_MAX, oldest dropped first) so it still follows genuine long-term drift, and it shares its lifecycle with SAMPLING_RATE_LIMITS_PER_SENSOR - clearMapOfSamplingRateLimitsPerSensor() now clears both, so measurements cannot leak across a CSV-set boundary. - Derive BOTH sides of the window from that median: limits[0] = median / SLOW_SENSOR_MAX_INTER_BLOCK_GAP_RATIO, limits[1] = median * FILE_GAP_TOLERANCE_MULTIPLIER.UPPER. The fast side previously used 1/minObservedPeriod, i.e. a single-sample extremum. - Median helper now averages the two middle values for an even-sized input (the old get(size/2) was the upper median) and sorts a copy. The data blocks' sampling rate is set from the accumulated median too, so the CSV timestamps and the continuity check agree on the achieved cadence. - Rewrote the stale/incorrect SLOW_SENSOR_MAX_INTER_BLOCK_GAP_RATIO javadoc: it no longer claims first-payload seeding (removed by #285) and no longer claims the ratio applies to both sides. - FILE_GAP_TOLERANCE_MULTIPLIER is now a static nested class. - New driver-side JUnit tests (API_00009_UtilCsvSplittingSlowSensorGapWindow, 12 cases, no hardware data needed) covering the even-count median, +12.5% healthy jitter staying continuous, a dropped block being detected even though its own payload fed the estimate, the measured window taking over from a fallback-seeded band (and the fallback not clobbering it back), history bounding and the clear contract. The unconditional put is preserved: the measured window must still win over the configured-rate +/-10% band that populateExpectedPayloadTsDiffLimitMapIfNeeded seeds, as soon as a measurement exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EmM3GD2F6zPqXmWHS94dh
Contributor
There was a problem hiding this comment.
Pull request overview
This PR improves slow-sensor (VD6283 / MLX90632) CSV gap detection by replacing per-payload window derivation with an accumulated, robust median-based window across a parse run, and adds targeted unit tests to prevent regressions.
Changes:
- Add per-sensor accumulated slow-sensor period history and robust median / symmetric-limit helpers in
UtilCsvSplitting. - Update
PayloadContentsDetailsV8orAbove.refineSlowSensorSamplingRateFromBlockTicksto feed per-payload measurements into the accumulator and apply the accumulated median to both timestamps (setSamplingRate) and the gap window. - Add a new JUnit4 test suite covering median behavior, window geometry, fallback interplay, and lifecycle/limits hygiene.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| ShimmerDriver/src/main/java/com/shimmerresearch/verisense/payloaddesign/UtilCsvSplitting.java | Adds accumulated-median history + window/median helpers and clears accumulated history alongside existing limits map. |
| ShimmerDriver/src/main/java/com/shimmerresearch/verisense/payloaddesign/PayloadContentsDetailsV8orAbove.java | Switches slow-sensor refinement to use the accumulated median for both sampling-rate limits and per-block sampling rate. |
| ShimmerDriver/src/test/java/com/shimmerresearch/verisense/payloaddesign/API_00009_UtilCsvSplittingSlowSensorGapWindow.java | Introduces regression tests for the new accumulated-median gap window behavior and related edge cases. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
remove(0) in a loop shifts the whole ArrayList once per removed element; clearing the excess head range via subList does it in a single pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EmM3GD2F6zPqXmWHS94dh
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.
Follow-up to the review on #285 (jyong15, 2026-08-20). Targets
DEV-793_slow_sensor_gap_windowso it can be merged into that PR before it lands on master. No version change inShimmerDriver/build.gradle.Findings addressed
1. MAJOR — self-referential gap window
refineSlowSensorSamplingRateFromBlockTicksre-derived the window on every payload from the same per-sample periods it then had to judge (perSamplePeriodsS[i] = (deltaTicks/32768.0)/sampleCount, and the checker computes exactly1/periodper boundary). With only 2-3 slow-sensor blocks per payload, a dropped block's 2x spacing was absorbed into the window that was supposed to catch it — and the healthy boundary back to the previous payload got flagged instead.Fix: the measured per-sample periods are now accumulated per sensor across the payloads of a parse run in
UtilCsvSplitting.SLOW_SENSOR_OBSERVED_PERIODS_PER_SENSOR, and the window is derived from a true median over that history rather than from the latest payload's values. A single 2x outlier in hundreds of measurements barely moves the median, so the real gap stays outside the window.2. MAJOR — fast side of the window
limits[1]usedFILE_GAP_TOLERANCE_MULTIPLIER.UPPER / minObservedPeriod— a single-sample extremum, so one fast spacing widened the fast edge for the whole recording. Both sides now come from the same robust median, in the newUtilCsvSplitting.calculateSlowSensorSamplingRateLimits(double):3. MINOR — upper-median
perSamplePeriodsS.get(size/2)is the upper median for even counts. The surviving median computation is nowUtilCsvSplitting.calculateMedian(List<Double>), which averages the two middle values for an even-sized input (and sorts a copy, so the caller's list is not re-ordered).4. MINOR — stale javadoc
SLOW_SENSOR_MAX_INTER_BLOCK_GAP_RATIO's javadoc no longer claims the window is "seeded from the first payload that carries >= 2 blocks" (behaviour removed by #285) and no longer claims 1.5x "keeps comfortable margin on both sides" (it only ever applied to the gap side). It now describes the accumulated-median basis and points at the method that builds the limits.5. MINOR — tests
New
ShimmerDriver/src/test/java/com/shimmerresearch/verisense/payloaddesign/API_00009_UtilCsvSplittingSlowSensorGapWindow.java— JUnit 4, matching the existingAPI_000NN_*naming/style inShimmerDriver/src/test. No hardware data or binary fixtures needed: the tests drive the window through per-sample periods exactly as the parser measures them, and the boundary rate the CSV splitter judges is simply1/period.6. NIT
FILE_GAP_TOLERANCE_MULTIPLIERis now astaticnested class.Design decisions
protected static HashMap<SENSORS, List<Double>>inUtilCsvSplitting, right next toSAMPLING_RATE_LIMITS_PER_SENSOR, keyed the same way.clearMapOfSamplingRateLimitsPerSensor()clears both, so measurements cannot leak across a CSV-set boundary (that method is the parser's per-CSV-set reset and has no in-repo caller — it is invoked from ASM_PC, which I could not inspect from here; the contract is kept and is asserted by a test). Keying bySENSORSrather thanDATABLOCK_SENSOR_IDkeeps the two maps' lifecycles literally identical; every sensor class key of a data block is fed the same measurements and therefore returns the same median.SLOW_SENSOR_PERIOD_HISTORY_MAX = 1024, oldest dropped first. Unbounded growth over a multi-day recording is avoided, and a sliding window (rather than a hard stop after N) means the median still follows genuine long-term drift in the sensor's cadence while remaining ~500 payloads deep — far too deep for individual dropped blocks to shift.populateExpectedPayloadTsDiffLimitMapIfNeededseeds a configured-rate ±10% band for a sensor that has not been measured yet (e.g. a first payload carrying <2 blocks of it), and the measured window overwrites it as soon as the first measurement exists. AcontainsKeyguard would lock the too-tight header estimate in for the whole file. Two tests pin both directions of this.setSamplingRatealso uses the accumulated median (review point 3's open question). The block sampling rate is what the block start times — and hence the CSV timestamps — are back-filled with. Using this payload's own median would stretch the sample spacing of exactly the payload that contains a dropped block, i.e. distort the data by the very artefact the window is meant to report. Keeping the timestamps and the continuity check on one estimate also stops them disagreeing about what the achieved cadence is. For the first payload of a recording the accumulated median equals the per-payload median, so nothing changes at the start of a file; later payloads simply get a steadier rate.UtilCsvSplitting(calculateMedian,calculateSlowSensorSamplingRateLimits,accumulateSlowSensorPeriodsAndGetMedianPeriodS) plusrefineSlowSensorSamplingRateLimitswhich accumulates and applies. All additive — no existing signature changed or removed.Test coverage added (12 cases)
testMedianAveragesTheTwoMiddleValuesForAnEvenCount,testAccumulatedMedianAveragesTheTwoMiddleValuesForAnEvenCounttestHealthyJitterStaysInsideTheWindow,testHealthyJitterStaysInsideTheWindowFromTheVeryFirstPayloadtestDroppedBlockIsDetectedEvenThoughItsPayloadFedTheEstimate, plustestPerPayloadWindowWouldHaveAbsorbedTheDroppedBlockwhich pins the old per-payload behaviour as the baseline it must not regress totestMeasuredWindowWinsOverAFallbackSeededBand,testFallbackSeedingDoesNotClobberAnExistingMeasuredWindowtestBothLimitsAreDerivedFromTheSameMediantestClearingTheLimitsMapAlsoClearsTheAccumulatedPeriods,testPeriodHistoryIsBounded,testNonPositiveAndMissingMeasurementsAreIgnoredValidation
No Gradle wrapper in the repo, so the environment's Gradle 8.14.3 was used from the
ShimmerDriverdirectory (there is no rootsettings.gradle):Test results from
ShimmerDriver/build/test-results/test: 63 tests, 0 failures, 0 errors, 0 skipped, of which 12 are the newAPI_00009_UtilCsvSplittingSlowSensorGapWindowcases; the pre-existing suites (API_00003,API_00004,API_00006,API_00007,API_00008,API_XXXXX_UtilParseData) all still pass.Not run here: any end-to-end parse against real DEV-927/SR68 recordings — those live in ASM_PC (
ASM_PC_00005_VerisenseFileParserPC) and neither the repository nor the binary fixtures were available in this environment. Re-running that suite before merge is still worthwhile, particularly to confirm the CSV timestamps produced by thesetSamplingRatechange on multi-payload slow-sensor recordings.🤖 Generated with Claude Code
https://claude.ai/code/session_014EmM3GD2F6zPqXmWHS94dh
Generated by Claude Code