Repository navigation
Unwrap the Kvaser 32-bit receive timer in the sidecar driver - #520
Merged
Merged
Conversation
This was referenced Sep 23, 2026
This was referenced Oct 6, 2026
Open
thebhef
marked this pull request as ready for review
October 14, 2026 21:08
Owner
Author
thebhef
changed the base branch from
task151-servers-section
to
graphite-base/520
October 14, 2026 21:53
python-can's Kvaser backend reads a frame's arrival out of `canReadWait` as a 32-bit count of 10 µs ticks and returns `ticks * TIMESTAMP_FACTOR + self._timestamp_offset` with no rollover handling — 4.6.1 (what we pin and freeze) and `main` as of 2026-09-23 both. The counter therefore rolls over every 2^32 ticks, 42,949.67296 s or just under 11 h 56 m, and every receive timestamp after the rollover is that much earlier than the one before it. On a Leaf v3 capture that cost 4 h 46 m of receive: the host dropped the rolled-back frames as falling before the session start, then accepted the rest 11.93 h stale. ADR 0024 is why that is fatal rather than cosmetic — a trace has one origin and everything is elapsed time from it. The sidecar's python-can driver now keeps the last *raw* stamp per open channel and counts a rollover when a new one falls more than half a period behind it, adding one period per rollover to every later stamp. Half a period is 5 h 58 m, which cannot be confused with the microsecond-wide reordering a receive queue delivers normally, and the comparison has to be against the raw value because the corrected one climbs out of the counter's range and a second rollover would then never look like one. Three things worth a reviewer's attention: - The correction is applied to `msg.timestamp` before the shared wire mapper reads it, not to the `Frame` after. The mapper rejects a stamp more than a day from the wall clock as driver garbage and substitutes the current time; a stamp two rollovers stale (23 h 51 m) is inside that window and a third is not, so correcting first is what keeps the mapper judging a stamp that is meant to be believable. - The Kvaser backend is identified by its own `_timestamp_offset` attribute, the way the PEAK path keys on `m_objPCANBasic` and the Vector path on `request_chip_state`. It is the only attribute in python-can with that name, and it is exactly the one whose arithmetic is wrong. No other backend is touched: a hardware stamp that is right is left alone, which is why restamping receive with the host clock was rejected. - The count restarts at zero on each open, and must: python-can re-derives `_timestamp_offset` from the live timer every time the bus goes on, so a reconfigure's new channel needs no correction until it rolls over itself, and the interface's report of how many rollovers subscribers have been told about resets with the state baseline alongside it. A corrected capture is indistinguishable from one that never wrapped, so the count is the only evidence the operator can be shown. The channel exposes it as `timer_wraps()` — optional on the driver protocol, like `rx_loss()`, and read by the state poll on the same 500 ms cadence for the same reason: an 11 h 56 m event can be reported half a second late, and the receive thread stays in its recv loop. Each increment fans out one WARN `LogMessage` naming the interface. Tests are hardware-free throughout: synthetic messages across one rollover and across two, the threshold pinned from both sides, a non-Kvaser bus left alone, a reopened channel counting from zero, and a missing stamp neither wrapping nor becoming the anchor; plus, at the interface, exactly one warning per rollover, silence from a backend with no such counter, and a reconfigure re-arming the report. The sidecar README's wire model documents the correction, and `plans/technology-inventory.md`'s python-can entry records the defect, the workaround, and the rejected alternative. An upstream python-can issue is drafted but not filed, pending the owner's confirmation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thebhef
force-pushed
the
task155-kvaser-unwrap
branch
from
October 14, 2026 21:58
68e0059 to
3f9b654
Compare
This was referenced Oct 15, 2026
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.
python-can's Kvaser backend reads a frame's arrival out of
canReadWaitas a 32-bit count of 10 µs ticks and returnsticks * TIMESTAMP_FACTOR + self._timestamp_offsetwith no rolloverhandling — 4.6.1 (what we pin and freeze) and
mainas of 2026-09-23both. The counter therefore rolls over every 2^32 ticks, 42,949.67296 s
or just under 11 h 56 m, and every receive timestamp after the rollover
is that much earlier than the one before it. On a Leaf v3 capture that
cost 4 h 46 m of receive: the host dropped the rolled-back frames as
falling before the session start, then accepted the rest 11.93 h stale.
ADR 0024 is why that is fatal rather than cosmetic — a trace has one
origin and everything is elapsed time from it.
The sidecar's python-can driver now keeps the last raw stamp per open
channel and counts a rollover when a new one falls more than half a
period behind it, adding one period per rollover to every later stamp.
Half a period is 5 h 58 m, which cannot be confused with the
microsecond-wide reordering a receive queue delivers normally, and the
comparison has to be against the raw value because the corrected one
climbs out of the counter's range and a second rollover would then
never look like one.
Three things worth a reviewer's attention:
msg.timestampbefore the shared wiremapper reads it, not to the
Frameafter. The mapper rejects a stampmore than a day from the wall clock as driver garbage and substitutes
the current time; a stamp two rollovers stale (23 h 51 m) is inside
that window and a third is not, so correcting first is what keeps the
mapper judging a stamp that is meant to be believable.
_timestamp_offsetattribute, the way the PEAK path keys on
m_objPCANBasicand theVector path on
request_chip_state. It is the only attribute inpython-can with that name, and it is exactly the one whose arithmetic
is wrong. No other backend is touched: a hardware stamp that is right
is left alone, which is why restamping receive with the host clock was
rejected.
re-derives
_timestamp_offsetfrom the live timer every time the busgoes on, so a reconfigure's new channel needs no correction until it
rolls over itself, and the interface's report of how many rollovers
subscribers have been told about resets with the state baseline
alongside it.
A corrected capture is indistinguishable from one that never wrapped,
so the count is the only evidence the operator can be shown. The
channel exposes it as
timer_wraps()— optional on the driverprotocol, like
rx_loss(), and read by the state poll on the same500 ms cadence for the same reason: an 11 h 56 m event can be reported
half a second late, and the receive thread stays in its recv loop. Each
increment fans out one WARN
LogMessagenaming the interface.Tests are hardware-free throughout: synthetic messages across one
rollover and across two, the threshold pinned from both sides, a
non-Kvaser bus left alone, a reopened channel counting from zero, and a
missing stamp neither wrapping nor becoming the anchor; plus, at the
interface, exactly one warning per rollover, silence from a backend
with no such counter, and a reconfigure re-arming the report.
The sidecar README's wire model documents the correction, and
plans/technology-inventory.md's python-can entry records the defect,the workaround, and the rejected alternative. An upstream python-can
issue is drafted but not filed, pending the owner's confirmation.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com