DEV-1023: reject an isolated zero timestamp instead of reading it as a wrap - #147
Conversation
…a wrap Firmware stamps a packet when the sample tick starts it and does not publish a packet it never stamped, so a counter field of exactly 0x000000 means the record is invalid rather than that the counter reached its origin. LogAndStream v1.00.x-v1.01.003 could emit one under SD write back-pressure. StreamTimeline read it as a 24-bit roll-over and, uniquely among the host implementations, split the damage across two samples: the zero advanced the timeline by the distance to the top of the range and the sample after it advanced by the rest, summing to exactly one modulo. Same net error as everywhere else - 512.000 s added to every later sample in the session. _unwrap now returns null for a 24-bit counter reading exactly zero whose predecessor was more than a second below the maximum, and stamp() holds every piece of timeline state where it was: _lastRaw so the next sample is compared against the last value the firmware actually stamped, _lastHostMs so the missed-wrap recovery still measures the whole gap, _wraps, and the pending anchor, which must not bind to a packet with no timestamp. The frame is still emitted, with ObjectCluster.timestampValid false, the way a CRC failure is: its sensor values are real and dropping it silently would hide the fault. The exemption is narrow. A genuine wrap onto zero has a predecessor at the top of the range and is accepted; the 16-bit counter, whose whole range is 2 s, is untouched; any other backward step is still a reorder or a wrap. Mirrors TimestampUnwrap in the Java driver and the C# API, and find_invalid_timestamps in pyshimmer. Five host implementations read the same wire format and have to agree on it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.
Tip: disable this comment in your organization's Code Review settings.
Adversarial review — no code changes needed, two notes for the reviewerWent looking for defects rather than confirmation. Nothing here needs changing, but two 1.
|
The fault
Firmware stamps a packet when the sample tick starts it and does not publish a packet
it never stamped, so a counter field of exactly
0x000000means the record isinvalid — not that the counter reached its origin. LogAndStream v1.00.x–v1.01.003
could emit one under SD write back-pressure.
StreamTimelineread it as a roll-over. Because this SDK uses a reorder-window rulerather than the naive "any backward step is a wrap", the damage arrived in two pieces
instead of one. With values taken from a real recording — predecessor
7406506, then0, on the 24-bit counter:forward = 9,370,710,backwards = 7,406,506— backwards exceeds the reorder window (2²¹), so it reads as forward motion7406571read forward from a predecessor of0The same net error as every other Shimmer host API, just split across two steps. The
host-clock missed-wrap branch does not engage — elapsed time between the two packets
is ~2 ms, far below half a modulo.
The fix
_unwrapreturnsnullfor a 24-bit counter reading exactly zero whose predecessorwas more than
INVALID_ZERO_WINDOW_TICKS(32768 — one second) below the maximum.stamp()then holds every piece of timeline state:_lastRaw, so the next sample is compared against the last value the firmwareactually stamped and reads as the ordinary step forward it is, rather than as a
second wrap;
_lastHostMs, so the recovery for wraps that went by unseen still measures thewhole gap rather than only the part after the bad packet;
_wraps;The frame is still emitted, with
ObjectCluster.timestampValidfalse, exactly the waya CRC failure is: its sensor values are real, and dropping it silently would hide the
fault from anything counting it. Its
TIMESTAMPfields repeat the previous frame's.Consumers that need a true time axis should drop those frames.
The exemption is deliberately narrow:
really can cross, is untouched;
Testing
12 new cases in
tests/core/stream-timeline.test.ts. Full suite 1873 passed / 95files;
typecheck,lintandprettierclean.Both defences were mutation-checked rather than trusted green:
_lastHostMsfails the missed-wrap-recovery case —which is why that test uses a 600 s gap with the bad packet landing late inside it,
the only shape where holding and not holding diverge.
Scope
This is one of four parallel host implementations of the same wire format; the
identical rule is going into the Java driver, the C# API and pyshimmer, so a
recording reads the same whichever reads it. Cross-referenced in the source comments.
No version bump and no
dist/vendor sync here — that is a release step, not part ofthe source fix.
DEV-1023.
🤖 Generated with Claude Code