Skip to content

Unwrap the Kvaser 32-bit receive timer in the sidecar driver - #520

Merged
thebhef merged 1 commit into
mainfrom
task155-kvaser-unwrap
Oct 14, 2026
Merged

thebhef merged 1 commit into
mainfrom
task155-kvaser-unwrap

Conversation

@thebhef

@thebhef thebhef commented Sep 23, 2026

Copy link
Copy Markdown
Owner

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

This was referenced Sep 23, 2026

thebhef commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner Author

Merge activity

  • Oct 14, 9:10 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Oct 14, 9:59 PM UTC: Graphite rebased this pull request as part of a merge.
  • Oct 14, 10:02 PM UTC: @thebhef merged this pull request with Graphite.

@thebhef
thebhef changed the base branch from task151-servers-section to graphite-base/520 October 14, 2026 21:53
@thebhef
thebhef changed the base branch from graphite-base/520 to main October 14, 2026 21:57
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
thebhef force-pushed the task155-kvaser-unwrap branch from 68e0059 to 3f9b654 Compare October 14, 2026 21:58
@thebhef
thebhef merged commit fe4fa6a into main Oct 14, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant