Skip to content

fix(canopen)!: remove SdoTransferMode.Expedited and .Segmented - #97

Merged
dborgards merged 7 commits into
mainfrom
fix/canopen-sdo-transfer-mode
Sep 12, 2026
Merged

dborgards merged 7 commits into
mainfrom
fix/canopen-sdo-transfer-mode

Conversation

@dborgards

@dborgards dborgards commented Sep 12, 2026 •

Copy link
Copy Markdown
Owner

What does this change?

SdoTransferMode.Expedited and .Segmented were an option surface the implementation ignored, so they are removed rather than implemented — the decision recorded on #44 and in ADR 0001. The rule that actually governs the transport (payload length on download, the server's initiate response on upload) is now written down on the enum, the interface, CanOpenNodeOptions.SdoBlockThresholdBytes and the package README, and pinned by tests.

This description was rewritten after review. An earlier version said Block is renumbered 3 → 1 and told callers to remap persisted values. That was wrong and has been reverted — see "What review changed" below. The commit message of 4289e6a still carries the superseded wording; that is a known wart with an owner decision attached, described at the end.

The premise checks out. Neither removed member ever reached the wire encoder:

  • CanOpenNode.SdoUploadAsync branches only on mode == Block; everything else goes to BeginSdoUpload, which sends a plain BuildUploadInit. The server's initiate response decides expedited vs. segmented, and the client handles both.
  • CanOpenNode.SdoDownloadAsync computes useBlock and then calls BeginSdoDownload → SdoFrames.BuildDownloadInit(index, subindex, payload). mode is never passed down. BuildDownloadInit picks expedited for data.Length <= 4 and segmented otherwise, from the length alone.

So Expedited and Segmented were indistinguishable from each other and from Auto below the block threshold, and the XML doc "Force the expedited codec (payloads 1..4 bytes only)" was false for a 100-byte payload passed with mode: Expedited — it went out segmented. Their one observable effect was incidental and undocumented: they suppressed the Auto → Block switch on downloads at or above SdoBlockThresholdBytes. Nothing in src/, samples/ or tests/ used either member.

Block keeps its numeric value 3, and the enum keeps the gap at 1 and 2. That is deliberate — see below.

What review changed

Three findings, all acted on.

1. Do not let Block reuse Expedited's value. The first revision renumbered Block 3 → 1 for tidiness. The pre-change enum is Auto = 0, Expedited = 1, Segmented = 2, Block = 3, so that put Block exactly on the value Expedited used to carry. Removing the members gives a recompiling caller a compile error — the loud failure this break wants — but an assembly still compiled against 1.2.x supplies the literal 1 in its IL, and so does a persisted or transmitted numeric value. Those callers would have gone from requesting a no-op hint to forcing block transfer: a real change on the wire that hangs against a peer with no block support. A gap in an enum costs nothing by comparison. Reverted in 4f2f115; the reasoning is recorded on the member so the gap is not "tidied" away later.

2. The public contract stated the CiA 301 split as if it were independent of the block threshold. Validate() only rejects SdoBlockThresholdBytes < 1, and SdoDownloadAsync tests the threshold first, so on a node configured with a threshold of 1..4 a one-byte payload goes by block transfer and "1..4 bytes expedited" is false. The internal comment at the selection site already had this right; the public docs had drifted. Five sites state the range — three already carried the qualification, two did not and now lead with the threshold (4f2f115).

3. An undefined SdoTransferMode selected a transport silently. Both entry points route "not Block" to the classic client, so a cast 1 or 2 — from stale IL, or from anyone following the superseded migration note — quietly changed the transport. ValidateTransferMode now throws ArgumentOutOfRangeException for anything but Auto and Block, synchronously, before a Task is returned, so an unawaited call cannot swallow it (1aaf8cf).

Not done, deliberately: requiring the threshold to exceed 4. Block transfer below five bytes is legal CiA 301, merely unusual, so rejecting it is a behavioural break stacked on an API break and would fail construction for anyone holding that configuration. Worth its own decision, not a silent extra here.

Type of change

  • feat — new behaviour (minor release)
  • fix / perf — bug or performance fix (patch release)
  • docs / test / refactor / chore / ci — no release
  • Breaking change (! in the title, plus a BREAKING CHANGE: footer in the commit)

Per ADR 0001 §7 the ! maps to a minor bump while the pre-1.3.0 window is open (.releaserc.json carries { "breaking": true, "release": "minor" }). Per ADR 0001 §4 no [Obsolete] shim is introduced — src/ still contains no [Obsolete] member.

Checklist

  • dotnet build CanKit.Pro.sln -c Release succeeds — run as -p:CI=true (warnings as errors): 0 Warning(s), 0 Error(s)
  • dotnet test CanKit.Pro.sln -c Release passes — --no-build --framework net10.0: 447 passed, 0 failed, 0 skipped
  • Public API changes are documented with XML comments
  • New behaviour is covered by a test
  • The requirement or ADR this relates to is referenced — ADR 0001, FR-CO-002 / FR-CO-003 / FR-CO-004

Also run: dotnet format CanKit.Pro.sln --verify-no-changes — clean (exit 0).

Tests

No existing test asserted the removed members, so there was nothing to delete. What was missing was a test of the rule the option obscured, plus one for the new guard.

CanOpenCodecTests pins SdoFrames.BuildDownloadInit:

  • 1, 2, 3 and 4 bytes → the expedited CS byte with the correct n (0x2F, 0x2B, 0x27, 0x23) and the payload in bytes 4..7.
  • 5, 17 and 1024 bytes → the segmented "size indicated" initiate (0x21) with the little-endian total length in bytes 4..7.

Sdo_Transfer_With_An_Undefined_Mode_Throws_Rather_Than_Picking_A_Transport covers the new guard as a theory over the two legacy literals (1, 2) and one never-defined value (99), on both upload and download. Verified to fail on all three cases with the validation removed, so it cannot pass vacuously.

The end-to-end halves already existed and still pass unchanged: Sdo_Expedited_Upload_ReturnsServerOdValue, Sdo_Expedited_Download_UpdatesServerOd, Sdo_Segmented_Upload_RoundTrip, Sdo_Segmented_Download_RoundTrip.

The API approval baseline was regenerated from the test's own .received.txt, not hand-written.

Migration

Drop the argument — it sends exactly the same frames:

// before — both produced identical traffic
await node.SdoDownloadAsync(id, index, sub, data, mode: SdoTransferMode.Expedited);
await node.SdoDownloadAsync(id, index, sub, data, mode: SdoTransferMode.Segmented);

// after
await node.SdoDownloadAsync(id, index, sub, data);

There is nothing to remap. Block keeps 3, so a persisted or transmitted enum value still means what it meant.

One behavioural difference. At or above SdoBlockThresholdBytes (default 128), a download that previously passed a removed mode bypassed block transfer as an undocumented side effect and now uses it. Raise the threshold if a peer cannot handle that.

Known wart: the footer on 4289e6a

That commit's BREAKING CHANGE: footer still describes the 3 → 1 renumbering and tells callers to remap. It is wrong, and because this repository merges rather than squashes, semantic-release reads it into the changelog. The footer on 4f2f115 opens by retracting it explicitly, but a reader who stops at the first note is still misled.

Correcting the text itself needs a history rewrite, which is not available to me in this environment. Three ways to close it, all the owner's call: amend 4289e6a (needs force-push), squash-merge this PR and drop the stale note from the squash message, or merge as-is and accept the retracted pair. 1aaf8cf is the mitigation either way — acting on the stale note now raises an immediate, named exception instead of silently changing the transport.

Closes #44.

🤖 Generated with Claude Code

https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj

Neither member ever reached the wire encoder. SdoUploadAsync routes
everything that is not Block through BeginSdoUpload, and SdoDownloadAsync
routes everything that is not block-selected through BeginSdoDownload ->
SdoFrames.BuildDownloadInit, which picks expedited for 1..4 bytes and
segmented above that from data.Length alone. Expedited and Segmented were
therefore indistinguishable from each other; their only observable effect
was the incidental one of suppressing the Auto -> Block switch on large
downloads, which is not what their XML docs promised ("Force the expedited
codec").

Per the decision recorded on the issue and ADR 0001, the members are
removed rather than implemented: forcing a codec the peer negotiates is a
test-harness concern, not something a production client should promise
forever. The payload-length rule is now documented on the enum, the
interface, CanOpenNodeOptions.SdoBlockThresholdBytes and the package
README.

Block is renumbered 3 -> 1 so the enum has no gap; pre-1.3.0 breaking
changes are allowed by ADR 0001 and no [Obsolete] shim is introduced.

New codec tests pin the rule the enum used to obscure: BuildDownloadInit
emits the expedited CS byte (with the correct n) for 1..4 bytes and the
segmented "size indicated" initiate for 5+ bytes, chosen by the
implementation rather than by a caller flag.

Closes #44

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj
@cursor

cursor Bot commented Sep 12, 2026 •

Copy link
Copy Markdown

PR Summary

Medium Risk
Breaking public enum/API plus SDO download transport selection changes above SdoBlockThresholdBytes for callers that relied on removed modes to avoid block transfer; new validation reduces silent mis-routing from stale enum values.

Overview
Breaking change: removes SdoTransferMode.Expedited and SdoTransferMode.Segmented from the public API. Only Auto and Block remain; Block stays 3 (intentional gap at 1/2 so stale numeric 1/2 does not silently map to a different wire transport).

SdoUploadAsync / SdoDownloadAsync now call ValidateTransferMode and throw ArgumentOutOfRangeException synchronously for any mode other than Auto/Block (covers legacy literals and invalid casts).

Docs and XML comments are aligned with actual behavior: expedited vs segmented is derived from payload length (and block threshold on download), not caller-selectable; upload Auto never auto-switches to block. README adds a 1.2.x migration section (including the one behavior change: large downloads that used removed modes no longer suppress block transfer).

Tests pin SdoFrames.BuildDownloadInit length rules and the new validation; API approval baseline drops the removed enum members.

Reviewed by Cursor Bugbot for commit 9a19447. Bugbot is set up for automated code reviews on this repo. Configure here.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4289e6a22c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/CanKit.Pro.CANopen/ICanOpenNode.cs Outdated
@codecov

codecov Bot commented Sep 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

claude and others added 2 commits September 12, 2026 19:21
…g-change footer

The removal commit marks the break with `!` in its header but carries no
BREAKING CHANGE footer, so the generated release notes would render the
subject line alone and none of the migration guidance. This repository
merges rather than squashes, so the footer has to live in a commit — the
pull request body is never read by the analyser.

The README gains the migration section the break needs: dropping the
argument sends identical frames, `Block` moves 3 -> 1, and the one real
behavioural difference is at or above SdoBlockThresholdBytes, where a
download that used to pass Expedited or Segmented bypassed block transfer
as an undocumented side effect and now uses it.

BREAKING CHANGE: `SdoTransferMode.Expedited` and `SdoTransferMode.Segmented`
are removed from `CanKit.Pro.CANopen`, and `SdoTransferMode.Block` changes
value from `3` to `1`. Neither removed member had any effect on the wire:
the expedited/segmented split is decided by the payload length on download
(1..4 bytes expedited, 5+ segmented) and by the server's initiate response
on upload. Migration: drop the argument —
`SdoDownloadAsync(id, idx, sub, data, mode: SdoTransferMode.Expedited)` and
`mode: SdoTransferMode.Segmented` both become
`SdoDownloadAsync(id, idx, sub, data)`, which sends exactly the same frames.
The one behavioural difference is at or above
`CanOpenNodeOptions.SdoBlockThresholdBytes` (default 128 bytes), where a
download that previously passed `Expedited` or `Segmented` bypassed block
transfer and now uses it; raise `SdoBlockThresholdBytes` on the node options
if that matters. Callers that persisted or transmitted the numeric enum
value must remap `3` to `1`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5aa8980d35

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/CanKit.Pro.CANopen/Sdo/SdoTransferMode.cs Outdated
…ed range by the threshold

Two review findings on the removal commit, both correct.

Renumbering Block 3 -> 1 to close the gap left by the removed members made
it reuse the value Expedited carried in 1.2.x. Removing the members gives a
recompiling caller a compile error, which is the loud failure this break
wants — but a caller that is not recompiled, or a persisted or transmitted
numeric value, supplies the literal 1 and would have gone from requesting a
no-op hint to forcing block transfer. That is a real change on the wire and
it hangs against a peer with no block support: the worst kind of break,
silent and remote. A gap in an enum costs nothing by comparison, so Block
keeps its value and the gap stays.

The public contract also stated the CiA 301 split as though it were
independent of the block threshold: "1..4 bytes expedited, 5 and up
segmented, and at or above SdoBlockThresholdBytes block transfer". The
threshold is tested first (CanOpenNode.SdoDownloadAsync), and the options
permit a threshold of 1..4, so on such a node a one-byte payload goes by
block transfer and the expedited claim is false. The internal comment at
the selection site already had this right; it is the public documentation
that was wrong, and it now says the threshold bounds the expedited range
too. Three of the five affected doc sites already carried the qualification
and are unchanged.

The API approval baseline was regenerated from the test's own .received.txt;
its only delta is the enum value.

Build 0 warnings / 0 errors with CI=true, suite 444/444, format clean.

BREAKING CHANGE: `SdoTransferMode.Expedited` and `SdoTransferMode.Segmented`
are removed from `CanKit.Pro.CANopen`. This supersedes the migration note on
the removal commit in one respect: **`SdoTransferMode.Block` keeps its value
`3` and there is nothing to remap.** That earlier note said Block moves from
`3` to `1`; it does not, because `1` is the value `Expedited` had in 1.2.x
and reusing it would silently turn a legacy caller's no-op hint into forced
block transfer. Everything else in that note stands: neither removed member
had any effect on the wire, so
`SdoDownloadAsync(id, idx, sub, data, mode: SdoTransferMode.Expedited)` and
`mode: SdoTransferMode.Segmented` both become
`SdoDownloadAsync(id, idx, sub, data)`, which sends exactly the same frames;
and at or above `CanOpenNodeOptions.SdoBlockThresholdBytes` (default 128) a
download that previously passed one of the removed modes bypassed block
transfer and now uses it, so raise the threshold if a peer cannot handle it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4f2f11522e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/CanKit.Pro.CANopen/Sdo/SdoTransferMode.cs
… picking a transport

Both entry points route "not Block" to the classic client, so a value that
is not a defined member selected a transport without complaint. That is the
wrong failure for an argument that can only arrive from a bug, and the
removed members make it reachable: an assembly still compiled against 1.2.x
supplies the literal 1 or 2 for Expedited and Segmented, and a caller
following an out-of-date migration note could pass the same. Silently
sending different frames than the caller asked for is diagnosed on the wire,
hours later, by someone who does not know the enum changed.

SdoUploadAsync and SdoDownloadAsync now throw ArgumentOutOfRangeException
for anything other than Auto or Block, synchronously, before a Task is
handed back so an unawaited call cannot swallow it. The message names the
removed members and says the argument should simply be dropped.

This is the code half of the review finding about the stale migration note
in the removal commit's footer. It does not correct that note — the text is
in an already-pushed commit message and semantic-release reads commit
messages, so fixing it needs a history rewrite that is not available here.
What it does is make following the stale advice fail loudly at the call
instead of quietly changing the transport, which is the harm the note could
otherwise cause.

Covered by Sdo_Transfer_With_An_Undefined_Mode_Throws_Rather_Than_Picking_A_Transport,
a theory over the two legacy literals and one never-defined value, verified
to fail on all three cases with the validation removed.

Build 0 warnings / 0 errors with CI=true, suite 447/447, format clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj
@dborgards
dborgards requested a balanced review from Copilot September 12, 2026 20:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1aaf8cf7ca

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/CanKit.Pro.CANopen/CanOpenNode.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Enum numeric-value guidance and threshold-related migration documentation are inconsistent and need correction.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (4)

src/CanKit.Pro.CANopen/CanOpenNode.cs:459

  • This new exception text says the removed members had “no effect on the wire,” but explicit value 1/2 did suppress the Auto→block switch for downloads at or above SdoBlockThresholdBytes; dropping the argument can therefore change those frames. The message should acknowledge that migration caveat and point callers to the threshold option rather than claiming the effect was always nil.
                $"and {nameof(SdoTransferMode.Block)} are defined; the removed Expedited (1) and " +
                "Segmented (2) members had no effect on the wire and their argument should simply " +
                "be dropped.");

src/CanKit.Pro.CANopen/CanOpenNodeOptions.cs:85

  • “Bypass the threshold in either direction” is misleading: Block can force block transfer below the threshold, but no mode can force the classic path for a download at or above the threshold; Auto already selects block there. Describe the one supported override explicitly.
    /// because the length is unknown until the server replies; callers can bypass the threshold
    /// in either direction by explicitly passing <see cref="Sdo.SdoTransferMode.Block"/>.

src/CanKit.Pro.CANopen/README.md:173

  • This repeats the inaccurate “no-op hint” characterization. The old values did not choose a codec, but they did keep large downloads on the classic non-block path; that is the observable behavior that would change if Block were renumbered to 1.
in 1.2.x, and an already-compiled caller passing that literal would have gone from requesting a
no-op hint to forcing block transfer — a silent change on the wire that hangs against a peer with
no block support. Removing the members gives such a caller a compile error instead, which is the

src/CanKit.Pro.CANopen/Sdo/SdoTransferMode.cs:47

  • Calling the old numeric values a “no-op hint” is inaccurate. In 1.2.x, values 1/2 selected the classic non-block path, so for downloads at or above the threshold they intentionally avoided block transfer even though they did not select expedited versus segmented. This matters to the rationale for preserving value 3 and should be documented accurately.
    /// it to <c>1</c> would silently reuse the value <c>Expedited</c> carried in 1.2.x: an
    /// already-compiled caller, or a persisted or transmitted numeric value, supplies the literal
    /// <c>1</c> and would go from requesting a no-op hint to forcing block transfer — a real
    /// change on the wire, and one that hangs against a peer with no block support. A compile
  • Files reviewed: 8/8 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread src/CanKit.Pro.CANopen/README.md Outdated
Comment thread src/CanKit.Pro.CANopen/Sdo/SdoTransferMode.cs
… the removed modes did

The message told the caller the removed members "had no effect on the wire"
and that the argument should "simply be dropped". The first half is the
premise of this PR and the second half follows from it, but only below
CanOpenNodeOptions.SdoBlockThresholdBytes. At or above the threshold the
removed members did have one observable effect: they suppressed the
Auto-to-Block switch. A caller who relied on that and follows the message
literally moves onto block transfer, which is the single migration step
that can hang against a peer with no block support.

So the exception now separates the two cases and names the threshold as the
knob for keeping the classic transport, and the remarks say explicitly why
the message must not be shortened back to "drop it". The README migration
section and the pull request description already carried the distinction;
the exception text was the one place that flattened it.

Text only -- no behaviour change. Build 0 warnings / 0 errors with CI=true,
suite 447/447, format clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj
Same overstatement as the exception message carried, in the sentence that
introduces the migration example: "dropping the argument sends exactly the
same frames" holds below CanOpenNodeOptions.SdoBlockThresholdBytes and not
at or above it, where the removed members suppressed the Auto-to-Block
switch. The section already said so under "One behavioural difference", but
a reader who takes the promise at the top and skims the example has no
reason to read on.

The claim is now bounded where it is made and points at the one case that
changes traffic, and the code comment in the example says "below the block
threshold" rather than an unqualified "identical traffic".

Build 0 warnings / 0 errors with CI=true, format clean. Markdown only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj

Copy link
Copy Markdown
Owner Author

CI: macos-latest red on 9a19447, and it is not this PR's

What failed. One test, J1939NodeTests.StartPeriodicSend_SingleFrame_FiresAtConfiguredPeriod:

Expected value to be between 84.0 and 192.0 because median inter-arrival (208.7 ms)
should approximate the configured period (120 ms), but found 208.709.

446 of 447 passed. ubuntu-latest green, build clean, format, version, validate release config, CodeQL, Cursor Bugbot and codecov all green. windows-latest still running at the time of writing.

Why it is not this PR's. The diff is eight files, all CANopen:

src/CanKit.Pro.CANopen/CanOpenNode.cs          src/CanKit.Pro.CANopen/README.md
src/CanKit.Pro.CANopen/CanOpenNodeOptions.cs   src/CanKit.Pro.CANopen/Sdo/SdoTransferMode.cs
src/CanKit.Pro.CANopen/ICanOpenNode.cs         tests/…/CanOpenCodecTests.cs
tests/…/ApiApprovals/CanKit.Pro.CANopen.approved.txt   tests/…/CanOpenNodeIntegrationTests.cs

Nothing under CanKit.Pro.J1939, CanKit.Pro.Actor or CanKit.Pro.Reliability. The failing test measures the inter-arrival time of J1939 periodic sends against a wall clock; this change cannot reach it.

It is #92, and specifically a test already tabulated there. #92 lists StartPeriodicSend_MultiFrame_KeepsFixedRate_Without_SendTime_Drift failing on macOS with a gap 196.9 ms off a 180 ms tolerance. This is its single-frame sibling in the same file, failing the same way: 208.7 ms against a 120 ms period with an 84–192 ms window. #92's own diagnosis applies unchanged — "a tolerance picked to survive the slowest runner ever observed is a number that gets widened again the next time" — so I have not touched the tolerance.

What I did. Attempted the one re-run the situation allows; GitHub refused it with 403 This workflow is already running, because windows-latest is still in flight in the same run. I will re-run the failed job once the run completes, and I am not merging this PR until it is green.

No fix to port. #92's durable fix is to drive the actor's DeadlineScheduler from a virtual clock the tests advance instead of measuring wall-clock time on a shared runner. That is a test-infrastructure change across several suites and has no business in a PR about SDO transfer modes.

Recorded on #92 as a sixth distinct test — the sixth in about an hour, none repeating.


Generated by Claude Code

@dborgards
dborgards merged commit a723b3c into main Sep 12, 2026
19 of 20 checks passed
@dborgards
dborgards deleted the fix/canopen-sdo-transfer-mode branch September 12, 2026 20:20
dborgards pushed a commit that referenced this pull request Sep 12, 2026
… comment that inverted them

Every J1939SpnValueKind summary described only the unsigned encoding, while
Classify returns the same members for signed SLOTs at the sign-bit-clear
codes. So the public documentation said "all raw bits set" for NotAvailable
where a signed field means -1, and named 0xFB / 0xFC..0xFD / 0xFE without
their 0x7B / 0x7C..0x7D / 0x7E counterparts. All five summaries, and the
IsNotAvailable property, now carry both forms.

Worse was the comment on the signed catalog test, which stated the rule
backwards -- "raw 0xFFFF is not available (leading byte 0xFF, sign bit
set)" -- directly above an assertion that 0x7FFF is the indicator. The code
was right and the prose was wrong, which is the dangerous way round: it
would have taught the inverted sentinel rule to whoever wrote the next
signed definition.

Rewriting that comment only replaced one unbacked claim with another, so
the rule it now states is pinned: raw 0xFFFF on the signed 16-bit vendor SPN
decodes to -0.1 deg, i.e. an ordinary -1 measurement rather than "not
available". That is precisely the confusion the signed encoding invites, and
nothing asserted it before.

Also merges main (a723b3c, #97).

Suite 583/583, build 0 warnings / 0 errors with CI=true, format clean.

Refs #37.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj
dborgards added a commit that referenced this pull request Sep 13, 2026
* fix(j1939)!: report SPN indicator ranges instead of scaling them

SAE J1939-71 §5.1.1 reserves the top of every SPN's raw range for the
"not available", "error", reserved and parameter-specific indicators.
J1939Spn.Extract ignored them and applied the linear transform to every
bit pattern, so an EEC1 frame from an ECU that does not have SPN 190
reported 8191.875 rpm instead of "no reading" — a plausible, wrong
measurement, which is the worst failure mode for diagnostic data.

Extraction now returns J1939SpnValue: a Kind (Valid, NotAvailable,
Error, Reserved, ParameterSpecific), the Raw bits as read off the wire,
and a Value that throws unless the field really carries a measurement.
TryGetValue and GetValueOrDefault (NaN by default) are the non-throwing
routes. A default-initialised J1939SpnValue reports NotAvailable rather
than a valid zero.

Signed SPNs are supported: J1939SpnDefinition gains IsSigned, Extract an
isSigned parameter, and ExtractRawSigned sign-extends a two's-complement
field. For a signed parameter the indicator codes sit at the top of the
signed range (0x7B..0x7F), so 0xFFFF is an ordinary -1, not "not
available". J1939Spn.Classify exposes the range check on its own.

Per ADR 0001 the return type is changed in place rather than added
beside the old one: a TryGetSpnValue next to a GetSpnValue that still
reported 0xFFFF as 8191.875 rpm would leave the defect reachable through
the API that is easiest to call. No [Obsolete] shim is introduced — the
ADR forbids one in the pre-1.3.0 window, and src/ still contains none.

The API approval baseline for CanKit.Pro.J1939 was regenerated from the
test's own .received.txt.

Closes #37. Refs FR-J1939-002, ADR 0001.

BREAKING CHANGE: J1939Spn.Extract, J1939SpnDefinition.Extract and
J1939SpnCatalog.Extract return J1939SpnValue instead of double, and the
J1939SpnDefinition primary constructor takes a trailing optional
IsSigned. Migration: `double rpm = J1939Spn.Extract(...)` becomes
`if (J1939Spn.Extract(...).TryGetValue(out double rpm)) { ... }`, or
`J1939Spn.Extract(...).GetValueOrDefault()` for a NaN-on-indicator
double. Reading `.Value` on an indicator throws InvalidOperationException
— that throw is the point: the old code silently produced a number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj

* test(j1939): pin the inferred sub-byte indicator widths, and name the 3-bit trade-off

Classify generalises the §5.1.1 tables to untabulated widths by keeping the
leading group a fixed size per width class. The tabulated widths were
covered; the inferred ones — 3, 5, 6 and 7 bits — were not, which is exactly
backwards: a reading the standard does not state is the one that can drift
without anyone noticing.

Twenty-six known-answer cases now pin each band at both boundaries. They
also make the consequence of the rule visible rather than implied: because
the leading group is fixed per class, the indicator fraction of the range is
whatever the tabulated width of that class already spends — about 2% for a
byte or wider, five sixteenths for 4..7 bits, one half for 2..3 bits.

That last case is worth a second look and is now named in the remarks: a
3-bit field classifies raw 4..7 as indicators, so a parameter genuinely
carrying eight states loses half of them. Scaling by value rather than by
leading group would cost two states instead of four. J1939-71 tabulates
neither, so the behaviour is left as it is and the choice is filed as #99
rather than changed here — it is a behavioural change to a public classifier
and does not belong in the pull request that introduced it.

No behaviour change: build is 0/0 with CI=true, suite 552/552, format clean.

Refs #37, #99, FR-J1939-002.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj

* test(j1939): cover the five patch lines Codecov flagged on the new SPN types

All five are behaviour that had no assertion, not lines that needed touching
for their own sake.

J1939SpnValue: TryGetValue and GetValueOrDefault were asserted only on the
indicator side, where they report "no reading". Their measurement side is
the path almost every caller takes and nothing pinned it, so a TryGetValue
returning false for a real measurement, or a GetValueOrDefault handing back
NaN for one, would have passed the suite. Now asserted, including that a
supplied default does not win over a real value. FromIndicator's second
guard -- a kind outside the defined range, from a cast integer or a member
added later without updating the check -- was never reached; a theory over
5, 99 and -1 reaches it.

J1939SpnDefinition: Name, Pgn and Unit had no assertion anywhere. A
definition is not only an extraction recipe, it is how a caller labels a
reading in a UI or a log, so a catalog entry could have carried the wrong
PGN or unit while every decode test passed. Covered for the built-in SPN 190
and through a Register round trip.

Both files are now 100% line and branch coverage, measured locally with
XPlat Code Coverage rather than assumed. The lines still uncovered in
J1939Spn.cs are the pre-existing argument guards in ExtractRaw and WriteRaw,
outside this change's diff, which is why the patch report does not name them.

Suite 573/573, build 0 warnings / 0 errors with CI=true, format clean.

Refs #37.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj

* docs(j1939): document the signed indicator ranges, and correct a test comment that inverted them

Every J1939SpnValueKind summary described only the unsigned encoding, while
Classify returns the same members for signed SLOTs at the sign-bit-clear
codes. So the public documentation said "all raw bits set" for NotAvailable
where a signed field means -1, and named 0xFB / 0xFC..0xFD / 0xFE without
their 0x7B / 0x7C..0x7D / 0x7E counterparts. All five summaries, and the
IsNotAvailable property, now carry both forms.

Worse was the comment on the signed catalog test, which stated the rule
backwards -- "raw 0xFFFF is not available (leading byte 0xFF, sign bit
set)" -- directly above an assertion that 0x7FFF is the indicator. The code
was right and the prose was wrong, which is the dangerous way round: it
would have taught the inverted sentinel rule to whoever wrote the next
signed definition.

Rewriting that comment only replaced one unbacked claim with another, so
the rule it now states is pinned: raw 0xFFFF on the signed 16-bit vendor SPN
decodes to -0.1 deg, i.e. an ordinary -1 measurement rather than "not
available". That is precisely the confusion the signed encoding invites, and
nothing asserted it before.

Also merges main (a723b3c, #97).

Suite 583/583, build 0 warnings / 0 errors with CI=true, format clean.

Refs #37.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj

---------

Co-authored-by: Claude <noreply@anthropic.com>
dborgards pushed a commit that referenced this pull request Sep 13, 2026
Two findings from the review of this branch, both caused by extending the
chronology from eight rows to eleven without re-reading what referred to it:

- The header still said eight merged pull requests while the inventory two
  paragraphs below listed nine (#93, #96, #97, #98, #100, #101, #104, #107,
  #108). Corrected to nine.
- A blank line between row 8 and row 9 terminated the Markdown table, so rows
  9-11 rendered as plain pipe-delimited text. Removed.

Two more of the same class that the review did not name: "in jedem der acht
Faelle" in the cause section refers to the measurement failures only, so it is
now "der ersten acht"; "von den acht Zeilen oben" means the whole table and is
now "elf".

The section on cross-paragraph contradictions records this occurrence, since
the document reproduced the very error class it describes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj
dborgards pushed a commit that referenced this pull request Sep 30, 2026
## [1.3.0](https://github.com/dborgards/CanKit.Pro/compare/v1.2.3...v1.3.0) (2026-09-30)

### ⚠ BREAKING CHANGES

* **isotp:** IIsoTpChannel.SettleAsync gained a CancellationToken
parameter. Callers compile unchanged; implementers of IIsoTpChannel and code
compiled against 1.2.x must be rebuilt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWRpkQzKkNDYz3WiNgWvWU
* **actor:** IProtocolActor.PostAsync and PostAsync<T> gained a
CancellationToken parameter. Callers compile unchanged; implementers of
IProtocolActor and code compiled against 1.2.x must be rebuilt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWRpkQzKkNDYz3WiNgWvWU
* **rawcan:** ICanBusService.SendConfirmed and CanBusService.SendConfirmed
are now SendConfirmedAsync. Signature and behaviour are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWRpkQzKkNDYz3WiNgWvWU
* **isotp:** a received escape-form First Frame with FF_DL <= 4095 is now
dropped instead of starting a reception. Peers that emit the escape form for
short lengths are no longer reassembled.
* **isotp:** IsoTpFrameCodec.BuildFirstFrame now throws
ArgumentOutOfRangeException for totalLength == 0 (it used to emit a malformed
frame), and IsoTpFrameCodec.TryParsePci returns false for a CAN-FD escape-form
First Frame with FF_DL == 0 (it used to return true). Callers must not announce
or accept zero-length segmented PDUs. Per ADR 0001 this maps to a minor bump
before v1.3.0.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RU3KbHNXhpyCjsn6QW9tee
* **j1939:** J1939Spn.Classify, and with it J1939Spn.Extract,
J1939Spn.FromRaw and J1939SpnDefinition.Extract, now report different
kinds for unsigned 3-, 5-, 6- and 7-bit fields. A 3-bit raw 4 or 5 is now
Valid (was Error) and raw 6 is Error (was NotAvailable). At 5..7 bits the
five codes are now the field's top five values, where they used to fill
the top five sixteenths of the range.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Zzhc7N2KFbUR4mr6TrWhT
* **canopen:** SdoUploadAsync and SdoDownloadAsync throw PeerSdoAccessException before any frame is sent when the (index, sub-index) is not declared by a description bound with BindPeerDeviceDescription. With no description bound, only 1000h:00, 1001h:00, 1018h:00 and 1018h:01 are accepted. 1003h and 1018h:02-04 are not exempt. A loaded file that omits 1000h, 1001h or 1018h rejects those objects too.

Co-authored-by: Dietmar Borgards <dborgards@users.noreply.github.com>
* **j1939tp:** J1939TpOptions.Th is renamed BamPacketSpacing, and the
* **isotp:** IIsoTpChannel gains DiscardPendingPdus(long). Source-
breaking for external implementers of the interface; consumers are
unaffected. Per docs/decisions/0001-versioning-and-api-stability.md this
is a minor bump before v1.3.0 and no shim is introduced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* **isotp:** IIsoTpChannel gains SettleAsync. Source-breaking for
external implementers of the interface; consumers are unaffected. Per
docs/decisions/0001-versioning-and-api-stability.md this is a minor bump
before v1.3.0 and no shim is introduced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* **isotp:** IIsoTpChannel.SendWithTransmitStampAsync returns
Task<IsoTpTransmitStamps> instead of Task<long>; read
LastFrameTransmitTimestamp for the previous value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* **j1939tp:** J1939TpOptions.Tr and the `tr` parameter of
J1939TpOptions.With are removed; T2 now bounds the receiver's CTS-to-first-DT
wait (its J1939-21 meaning) and the originator's post-block CTS wait is
bounded by T3. J1939TpAbortReason.UnexpectedCtsNumPackets,
UnexpectedCtsSequenceNumber, RetransmitNotSupported,
ResourceNeededForHigherPriorityProcess and ReceiverAbort are replaced by the
table 7 members; SessionAlreadyOpen's value changes from 7 to 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* **isotp:** IIsoTpChannel gains GetReceptionsInProgress() (added as
TryGetReceptionInProgress in 7099e0a, reshaped since); implementations
outside this repository must add it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* **isotp:** ISubscription gains WaitToReadAsync(CancellationToken);
implementations outside this repository must add it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* **rawcan:** TxConfirmation gains HostTransmitTimestamp. It is an init-only
property on a record struct, so existing construction and consumption compile
unchanged; the public shape changes nonetheless. Per
docs/decisions/0001-versioning-and-api-stability.md this is a minor bump before
v1.3.0. API approval baseline taken from the generated *.received.txt.
* **uds:** IIsoTpChannel gains SendWithTransmitStampAsync. Source-breaking
for external implementers of the interface; SendAsync is unchanged for consumers
and now delegates to it. Per docs/decisions/0001-versioning-and-api-stability.md
this is a minor bump before v1.3.0 and no shim is introduced.
* **uds:** IIsoTpChannel gains TryReceiveWithArrival. Source-breaking for
external implementers of the interface; no consumer of a channel is affected.
Per docs/decisions/0001-versioning-and-api-stability.md this is a minor bump
before v1.3.0 and no shim is introduced.
* **rawcan:** CanFrameEvent gains a fourth constructor parameter. It is
optional, so existing calls compile unchanged; the type's public shape changes
nonetheless. Per docs/decisions/0001-versioning-and-api-stability.md this is a
minor bump before v1.3.0. API approval baseline taken from the generated
*.received.txt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj
* **isotp:** IIsoTpChannel gains ReceiveWithArrivalAsync, so any external
implementation of the interface must add it. The only implementation in this
repository is internal. Per docs/decisions/0001-versioning-and-api-stability.md
this is a minor bump before v1.3.0, and no [Obsolete] shim is introduced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj
* **j1939:** J1939Spn.Extract, J1939SpnDefinition.Extract and
J1939SpnCatalog.Extract return J1939SpnValue instead of double, and the
J1939SpnDefinition primary constructor takes a trailing optional
IsSigned. Migration: `double rpm = J1939Spn.Extract(...)` becomes
`if (J1939Spn.Extract(...).TryGetValue(out double rpm)) { ... }`, or
`J1939Spn.Extract(...).GetValueOrDefault()` for a NaN-on-indicator
double. Reading `.Value` on an indicator throws InvalidOperationException
— that throw is the point: the old code silently produced a number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj

* test(j1939): pin the inferred sub-byte indicator widths, and name the 3-bit trade-off

Classify generalises the §5.1.1 tables to untabulated widths by keeping the
leading group a fixed size per width class. The tabulated widths were
covered; the inferred ones — 3, 5, 6 and 7 bits — were not, which is exactly
backwards: a reading the standard does not state is the one that can drift
without anyone noticing.

Twenty-six known-answer cases now pin each band at both boundaries. They
also make the consequence of the rule visible rather than implied: because
the leading group is fixed per class, the indicator fraction of the range is
whatever the tabulated width of that class already spends — about 2% for a
byte or wider, five sixteenths for 4..7 bits, one half for 2..3 bits.

That last case is worth a second look and is now named in the remarks: a
3-bit field classifies raw 4..7 as indicators, so a parameter genuinely
carrying eight states loses half of them. Scaling by value rather than by
leading group would cost two states instead of four. J1939-71 tabulates
* **rawcan:** ICanBusService.FindOverlappingFilterSubscriptions() now returns
IReadOnlyList<FilterOverlap> instead of
IReadOnlyList<(ISubscription First, ISubscription Second)>. Positional
destructuring -- foreach (var (a, b) in ...) -- keeps working unchanged.
Member access moves from pair.First/pair.Second to overlap.A/overlap.B, and the
overlapping ID range is available as overlap.LowestSharedId/HighestSharedId.
Implementations of ICanBusService must update the member's return type.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj
* **canopen:** footer, so the generated release notes would render the
subject line alone and none of the migration guidance. This repository
merges rather than squashes, so the footer has to live in a commit — the
pull request body is never read by the analyser.

The README gains the migration section the break needs: dropping the
argument sends identical frames, `Block` moves 3 -> 1, and the one real
behavioural difference is at or above SdoBlockThresholdBytes, where a
download that used to pass Expedited or Segmented bypassed block transfer
as an undocumented side effect and now uses it.
* **canopen:** `SdoTransferMode.Expedited` and `SdoTransferMode.Segmented`
are removed from `CanKit.Pro.CANopen`, and `SdoTransferMode.Block` changes
value from `3` to `1`. Neither removed member had any effect on the wire:
the expedited/segmented split is decided by the payload length on download
(1..4 bytes expedited, 5+ segmented) and by the server's initiate response
on upload. Migration: drop the argument —
`SdoDownloadAsync(id, idx, sub, data, mode: SdoTransferMode.Expedited)` and
`mode: SdoTransferMode.Segmented` both become
`SdoDownloadAsync(id, idx, sub, data)`, which sends exactly the same frames.
The one behavioural difference is at or above
`CanOpenNodeOptions.SdoBlockThresholdBytes` (default 128 bytes), where a
download that previously passed `Expedited` or `Segmented` bypassed block
transfer and now uses it; raise `SdoBlockThresholdBytes` on the node options
if that matters. Callers that persisted or transmitted the numeric enum
value must remap `3` to `1`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj

* fix(canopen)!: keep SdoTransferMode.Block at 3, and bound the expedited range by the threshold

Two review findings on the removal commit, both correct.

Renumbering Block 3 -> 1 to close the gap left by the removed members made
it reuse the value Expedited carried in 1.2.x. Removing the members gives a
recompiling caller a compile error, which is the loud failure this break
wants — but a caller that is not recompiled, or a persisted or transmitted
numeric value, supplies the literal 1 and would have gone from requesting a
no-op hint to forcing block transfer. That is a real change on the wire and
it hangs against a peer with no block support: the worst kind of break,
silent and remote. A gap in an enum costs nothing by comparison, so Block
keeps its value and the gap stays.

The public contract also stated the CiA 301 split as though it were
independent of the block threshold: "1..4 bytes expedited, 5 and up
segmented, and at or above SdoBlockThresholdBytes block transfer". The
threshold is tested first (CanOpenNode.SdoDownloadAsync), and the options
permit a threshold of 1..4, so on such a node a one-byte payload goes by
block transfer and the expedited claim is false. The internal comment at
the selection site already had this right; it is the public documentation
that was wrong, and it now says the threshold bounds the expedited range
too. Three of the five affected doc sites already carried the qualification
and are unchanged.

The API approval baseline was regenerated from the test's own .received.txt;
its only delta is the enum value.

Build 0 warnings / 0 errors with CI=true, suite 444/444, format clean.
* **canopen:** `SdoTransferMode.Expedited` and `SdoTransferMode.Segmented`
are removed from `CanKit.Pro.CANopen`. This supersedes the migration note on
the removal commit in one respect: **`SdoTransferMode.Block` keeps its value
`3` and there is nothing to remap.** That earlier note said Block moves from
`3` to `1`; it does not, because `1` is the value `Expedited` had in 1.2.x
and reusing it would silently turn a legacy caller's no-op hint into forced
block transfer. Everything else in that note stands: neither removed member
had any effect on the wire, so
`SdoDownloadAsync(id, idx, sub, data, mode: SdoTransferMode.Expedited)` and
`mode: SdoTransferMode.Segmented` both become
`SdoDownloadAsync(id, idx, sub, data)`, which sends exactly the same frames;
and at or above `CanOpenNodeOptions.SdoBlockThresholdBytes` (default 128) a
download that previously passed one of the removed modes bypassed block
transfer and now uses it, so raise the threshold if a peer cannot handle it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj

* fix(canopen): reject an undefined SdoTransferMode instead of silently picking a transport

Both entry points route "not Block" to the classic client, so a value that
is not a defined member selected a transport without complaint. That is the
wrong failure for an argument that can only arrive from a bug, and the
removed members make it reachable: an assembly still compiled against 1.2.x
supplies the literal 1 or 2 for Expedited and Segmented, and a caller
following an out-of-date migration note could pass the same. Silently
sending different frames than the caller asked for is diagnosed on the wire,
hours later, by someone who does not know the enum changed.

SdoUploadAsync and SdoDownloadAsync now throw ArgumentOutOfRangeException
for anything other than Auto or Block, synchronously, before a Task is
handed back so an unawaited call cannot swallow it. The message names the
removed members and says the argument should simply be dropped.

This is the code half of the review finding about the stale migration note
in the removal commit's footer. It does not correct that note — the text is
in an already-pushed commit message and semantic-release reads commit
messages, so fixing it needs a history rewrite that is not available here.
What it does is make following the stale advice fail loudly at the call
instead of quietly changing the transport, which is the harm the note could
otherwise cause.

Covered by Sdo_Transfer_With_An_Undefined_Mode_Throws_Rather_Than_Picking_A_Transport,
a theory over the two legacy literals and one never-defined value, verified
to fail on all three cases with the validation removed.

Build 0 warnings / 0 errors with CI=true, suite 447/447, format clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj

* docs(canopen): stop the undefined-mode message from understating what the removed modes did

The message told the caller the removed members "had no effect on the wire"
and that the argument should "simply be dropped". The first half is the
premise of this PR and the second half follows from it, but only below
CanOpenNodeOptions.SdoBlockThresholdBytes. At or above the threshold the
removed members did have one observable effect: they suppressed the
Auto-to-Block switch. A caller who relied on that and follows the message
literally moves onto block transfer, which is the single migration step
that can hang against a peer with no block support.

So the exception now separates the two cases and names the threshold as the
knob for keeping the classic transport, and the remarks say explicitly why
the message must not be shortened back to "drop it". The README migration
section and the pull request description already carried the distinction;
the exception text was the one place that flattened it.

Text only -- no behaviour change. Build 0 warnings / 0 errors with CI=true,
suite 447/447, format clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj

* docs(canopen): qualify the migration claim in the README the same way

Same overstatement as the exception message carried, in the sentence that
introduces the migration example: "dropping the argument sends exactly the
same frames" holds below CanOpenNodeOptions.SdoBlockThresholdBytes and not
at or above it, where the removed members suppressed the Auto-to-Block
switch. The section already said so under "One behavioural difference", but
a reader who takes the promise at the top and skims the example has no
reason to read on.

The claim is now bounded where it is made and points at the one case that
changes traffic, and the code comment in the example says "below the block
threshold" rather than an unqualified "identical traffic".

Build 0 warnings / 0 errors with CI=true, format clean. Markdown only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj
* **rawcan:** CanIdFilter.Range and CanIdFilter.Mask now throw
ArgumentOutOfRangeException for bounds or acceptance pairs outside the target ID
space. Code that built such a filter was matching nothing at all -- the fix is
to pass CanFilterIDType.Extend for 29-bit IDs, or to correct the bound. Filters
already inside their ID space are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj
* **rawcan:** `ISubscription.Frames` yields `CanFrameEvent` instead of
`CanFrameView`, `TryRead` has an `out CanFrameEvent`, and subscription
predicates and callbacks take `CanFrameEvent`. Reach the frame through
`.Frame`. On a bus not configured for echo nothing else changes; on an echo bus
a subscription withholds host echoes unless it passes `includeEcho: true`. Note
that the flag is host-scoped, so a protocol layer sharing one `ICanBusService`
with siblings must opt in and identify its own traffic itself -- every layer in
this repository now does. `CanFrameEvent` equality compares payload bytes
rather than the backing buffer. Per docs/decisions/0001-versioning-and-api-stability.md
this ships as a minor bump, not a major one: 1.3.0 is the first release for
which the SemVer promise holds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj
* **rawcan:** compare CanFrameEvent by payload bytes, and correct the stale narrative
* **isotp:** opt subscriptions into echoes on a shared service
* **rawcan:** the echo flag is host-scoped, so protocol layers opt in
* **j1939tp:** keep the source-address self-check behind the echo gate
* **rawcan:** `ISubscription.Frames` yields `CanFrameEvent` instead of
`CanFrameView`, `TryRead` has an `out CanFrameEvent`, and subscription
predicates are `Func<CanFrameEvent, bool>`. Reach the frame through
`.Frame`. Echo frames are no longer delivered to a subscription that did not
pass `includeEcho: true`; on a bus not configured for echo nothing changes.
Per docs/decisions/0001-versioning-and-api-stability.md this is a minor bump,
not a major one: 1.3.0 is the first release for which the SemVer promise holds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj

### Features

* **actor:** let IProtocolActor.PostAsync be withdrawn with a CancellationToken ([6516b28](https://github.com/dborgards/CanKit.Pro/commit/6516b28c93aac25e234ce29a8c6bf656bbe9e39b))
* **addressing:** take the frame's kind into Decompose, refuse a PDU1 PGN with a low byte, and give the NAME its wire order ([9d9e45e](https://github.com/dborgards/CanKit.Pro/commit/9d9e45e692460e5ec79a77838d90b9ff6720ec2a)), closes [#55](https://github.com/dborgards/CanKit.Pro/issues/55)
* **canopen:** add listen-only discovery and the scan on request ([c24b368](https://github.com/dborgards/CanKit.Pro/commit/c24b36880983e06e379ac0a29cac318b5ba1ebd8)), closes [#131](https://github.com/dborgards/CanKit.Pro/issues/131)
* **canopen:** add SdoAbortCode.InvalidSequenceNumber and ValueRangeExceeded ([56df0b5](https://github.com/dborgards/CanKit.Pro/commit/56df0b5db14115939e39da7e91b799cdfac943fc)), closes [#39](https://github.com/dborgards/CanKit.Pro/issues/39)
* **canopen:** boot assigned slaves after the flying master wins ([a63422f](https://github.com/dborgards/CanKit.Pro/commit/a63422f2d0c3113c6d2c7b905af34f43276ed06f))
* **canopen:** decode a peer PDO from its live mapping or from the EDS ([5571517](https://github.com/dborgards/CanKit.Pro/commit/5571517699356cbc9ab776d3b05b2ebefebc3651))
* **canopen:** default 1F80h from the node profile and hold the master ([dc5131f](https://github.com/dborgards/CanKit.Pro/commit/dc5131fbc3b83f08bfcd9a609f457ac8ec32a52e))
* **canopen:** derive the node's communication configuration from its object dictionary ([196eaa8](https://github.com/dborgards/CanKit.Pro/commit/196eaa8fa5ae08aa908552d532795dfd131cbad2)), closes [#41](https://github.com/dborgards/CanKit.Pro/issues/41) [#40](https://github.com/dborgards/CanKit.Pro/issues/40) [#42](https://github.com/dborgards/CanKit.Pro/issues/42) [#43](https://github.com/dborgards/CanKit.Pro/issues/43)
* **canopen:** elect an active NMT master with flying master ([8beed02](https://github.com/dborgards/CanKit.Pro/commit/8beed026beeb581a48ec3ab923765a01f04a89d5))
* **canopen:** gate client SDO on a peer EDS or DCF ([056fbad](https://github.com/dborgards/CanKit.Pro/commit/056fbad3a7979b893f454e1ac98735e4f6f19994))
* **canopen:** load the object dictionary from an EDS or DCF device description ([74f3c4a](https://github.com/dborgards/CanKit.Pro/commit/74f3c4a3144ac8b9bb0fae2d073e1f32a3a233f3)), closes [#132](https://github.com/dborgards/CanKit.Pro/issues/132) [#132](https://github.com/dborgards/CanKit.Pro/issues/132)
* **canopen:** prefer the live COB-ID and mapping over the EDS ([b544f21](https://github.com/dborgards/CanKit.Pro/commit/b544f21befaa25dabaf3c223af65c0725e09b33b))
* **canopen:** raise ApplicationReset before the boot-up and send every EMCY through one ordered path ([236c574](https://github.com/dborgards/CanKit.Pro/commit/236c57418358da28ba02c3e547b3b5490a5d0f44))
* **canopen:** SdoAbortException says which side aborted ([#59](https://github.com/dborgards/CanKit.Pro/issues/59)) ([c9967c3](https://github.com/dborgards/CanKit.Pro/commit/c9967c36dd6c8c1ebf7d7c84423ab55ccd36400c))
* **isotp): stamp a functional response with its arrival; fix(uds:** note a suppressed send's window before the send, and run P2* from the 0x78's arrival ([1dc74ac](https://github.com/dborgards/CanKit.Pro/commit/1dc74ac77b8e20924d030415e5e750cb8950577f)), closes [#150](https://github.com/dborgards/CanKit.Pro/issues/150)
* **isotp:** let a channel be settled, so a look at the inbox at a deadline sees every frame that arrived before it ([c3c025b](https://github.com/dborgards/CanKit.Pro/commit/c3c025be307ead9b29fc550fc947e3995276beee))
* **isotp:** let a discard take the caller's stamp, so what it drops is what the caller has read ([a8b54a1](https://github.com/dborgards/CanKit.Pro/commit/a8b54a1c97528d3a14f809c5fbca338004b31ca6))
* **isotp:** let a functional client listen on one subscription across collections ([3728387](https://github.com/dborgards/CanKit.Pro/commit/37283877c1069d42fa23d16618c17d9bc6f96e89)), closes [#150](https://github.com/dborgards/CanKit.Pro/issues/150)
* **isotp:** let IIsoTpChannel.SettleAsync be cancelled with a CancellationToken ([3b9546f](https://github.com/dborgards/CanKit.Pro/commit/3b9546f984b7b38f1bc5635ea921a830f5c1aad2))
* **isotp:** report the arrival instant of a received PDU ([88099eb](https://github.com/dborgards/CanKit.Pro/commit/88099ebc59546c3fa069829fac10258f68b3038a))
* **isotp:** report when a functional request went out, and leave out a response from before its handoff ([c2dbc0b](https://github.com/dborgards/CanKit.Pro/commit/c2dbc0b4a3163c044673aaf5eb1e9e00115cbce2))
* **j1939tp:** bound the per-destination TX queue and drop cancelled entries ([742b87c](https://github.com/dborgards/CanKit.Pro/commit/742b87ce735d1540637aba36016eba8115193f0b)), closes [#204](https://github.com/dborgards/CanKit.Pro/issues/204)
* **j1939tp:** serve retransmit requests, read a PDU1 PGN as the group it names, refuse an RTS allowing no packet, deliver before the event, and name the BAM spacing what it is ([7130884](https://github.com/dborgards/CanKit.Pro/commit/713088449eb9e0a300355fd4270df062d817f035)), closes [#58](https://github.com/dborgards/CanKit.Pro/issues/58) [#55](https://github.com/dborgards/CanKit.Pro/issues/55) [#144](https://github.com/dborgards/CanKit.Pro/issues/144)
* **rawcan:** carry the echo flag and receive timestamp to subscribers ([7831d69](https://github.com/dborgards/CanKit.Pro/commit/7831d69db2941aa639e0642ccd9378108a51a5a4)), closes [#23](https://github.com/dborgards/CanKit.Pro/issues/23) [#19](https://github.com/dborgards/CanKit.Pro/issues/19)
* **rawcan:** give the callback Subscribe an onError channel, and stop the pump from failing unobserved ([11cd32c](https://github.com/dborgards/CanKit.Pro/commit/11cd32cc0c20ae1aa6bb0e208391d7c7eb48c59f)), closes [#53](https://github.com/dborgards/CanKit.Pro/issues/53) [#53](https://github.com/dborgards/CanKit.Pro/issues/53) [#53](https://github.com/dborgards/CanKit.Pro/issues/53)
* **rawcan:** replace the overlap tuple pair with a named FilterOverlap carrying the shared ID range ([46db7cb](https://github.com/dborgards/CanKit.Pro/commit/46db7cbe3a89f2bcccb195f042c7a5f55175896d)), closes [#82](https://github.com/dborgards/CanKit.Pro/issues/82) [#82](https://github.com/dborgards/CanKit.Pro/issues/82) [#82](https://github.com/dborgards/CanKit.Pro/issues/82)
* **rawcan:** report the instant before the driver call in a confirmation ([89fe4ff](https://github.com/dborgards/CanKit.Pro/commit/89fe4ffbdf7a18df1cd090a3ab15255a429443a5))
* **rawcan:** stamp a frame in the demux, before any subscription buffers it ([4f77091](https://github.com/dborgards/CanKit.Pro/commit/4f770910a703d282441cd71d9dc4267858b44ff7))
* **uds:** the collected UDS findings -- busy repeat, functional addressing, and four corrections ([e460d7f](https://github.com/dborgards/CanKit.Pro/commit/e460d7fa6fd1c5d5a37e9532dbd47055c7d86af2))

### Bug Fixes

* **actor:** dispose a withdrawn PostAsync call and tidy its ownership ([f9639e7](https://github.com/dborgards/CanKit.Pro/commit/f9639e7a9eea9d8837899b739523a27ca68451c0))
* **actor:** let the withdraw transition decide a dispatch failure too ([78b4cd8](https://github.com/dborgards/CanKit.Pro/commit/78b4cd8cc41cf8f4eccfc9a645f25e78a9191ebc))
* **actor:** mark marshal-failure handlers as on-loop ([647feb1](https://github.com/dborgards/CanKit.Pro/commit/647feb1354ca32be39502435833619e19aac556e))
* **actor:** measure deadlines on a monotonic clock, keep timers fair ([869756d](https://github.com/dborgards/CanKit.Pro/commit/869756d8ea3cb026bc8400591a6519df9a3bb273)), closes [#20](https://github.com/dborgards/CanKit.Pro/issues/20) [#21](https://github.com/dborgards/CanKit.Pro/issues/21) [#19](https://github.com/dborgards/CanKit.Pro/issues/19) [#54](https://github.com/dborgards/CanKit.Pro/issues/54)
* **actor:** release the loop count and disposables when the loop fails to start ([e1dbd85](https://github.com/dborgards/CanKit.Pro/commit/e1dbd8584c36882b7eec6434d467eaef9138a6ee)), closes [#209](https://github.com/dborgards/CanKit.Pro/issues/209)
* **actor:** release the token registration when PostAsync is refused ([dd1a4ab](https://github.com/dborgards/CanKit.Pro/commit/dd1a4ab9af4afb44d1b6d92cf929002200f441c8))
* **build:** repair the XML comment that broke every build leg ([ff7012a](https://github.com/dborgards/CanKit.Pro/commit/ff7012a2a7d7918581b2cee965efe232012ffff6))
* **canopen:** abort a block-upload initiate with an invalid blksize ([#59](https://github.com/dborgards/CanKit.Pro/issues/59)) ([8109cf7](https://github.com/dborgards/CanKit.Pro/commit/8109cf7beaa043388bca79c658b43eb7d2a4308b))
* **canopen:** accept a 1016h count only for sub-indices that exist ([948366c](https://github.com/dborgards/CanKit.Pro/commit/948366c0d0e6e6c1d4965491aa7e5e0e0f2789f0))
* **canopen:** accept a block transfer of exactly MaxSdoTransferBytes ([#59](https://github.com/dborgards/CanKit.Pro/issues/59)) ([6cd7632](https://github.com/dborgards/CanKit.Pro/commit/6cd763295f9ee7d9b6927832f840b6421bc2b72f))
* **canopen:** allow the full 1018h identity without a peer file ([8c87878](https://github.com/dborgards/CanKit.Pro/commit/8c87878a8a47d1d93246d1a9f31bc28893316977))
* **canopen:** an accepted block-upload segment settles earlier sends ([01dbd92](https://github.com/dborgards/CanKit.Pro/commit/01dbd92589d31656b666c3669d827ae6b618e250))
* **canopen:** any in-range upload segment settles the earlier sends ([ffd450f](https://github.com/dborgards/CanKit.Pro/commit/ffd450ff00f81687be030a612cf5491373cfa3ce))
* **canopen:** apply a sub-block confirm's blksize to the following sub-block, not to the retransmission ([2571ccf](https://github.com/dborgards/CanKit.Pro/commit/2571ccf0d5be7eb3812e28e12087083ec3ac6870)), closes [#134](https://github.com/dborgards/CanKit.Pro/issues/134) [#133](https://github.com/dborgards/CanKit.Pro/issues/133)
* **canopen:** arm the life time before the guarding reply leaves, and the producer before a reset's boot-up ([293882f](https://github.com/dborgards/CanKit.Pro/commit/293882fe3e41ba16d5a8a52358a976d00a340daa)), closes [#141](https://github.com/dborgards/CanKit.Pro/issues/141)
* **canopen:** bound the critical event backlog behind a stuck handler ([1749427](https://github.com/dborgards/CanKit.Pro/commit/1749427b761a47e7b03ff6802538384a45398634)), closes [#201](https://github.com/dborgards/CanKit.Pro/issues/201)
* **canopen:** check a 1016h count for duplicate producers and keep every heartbeat behind the reset boot-up ([f3ebfa4](https://github.com/dborgards/CanKit.Pro/commit/f3ebfa46803f7db02369aba02ae3053124535d49))
* **canopen:** close the review findings on the OD-driven node ([f8e7915](https://github.com/dborgards/CanKit.Pro/commit/f8e79150862876a8b2e3cdb836c28e42c42fd503)), closes [#133](https://github.com/dborgards/CanKit.Pro/issues/133)
* **canopen:** coalesce event-queue wakeups ([e5134f8](https://github.com/dborgards/CanKit.Pro/commit/e5134f8db1795067d0ed63576c2d7a248ddb17f5))
* **canopen:** complete a block upload only once its end ack is sent ([3247f27](https://github.com/dborgards/CanKit.Pro/commit/3247f27351c5343683170e0e43cc678bff959c45))
* **canopen:** confirm a damaged sub-block once; no initiate guessing in the segment phase ([#39](https://github.com/dborgards/CanKit.Pro/issues/39)) ([21656f0](https://github.com/dborgards/CanKit.Pro/commit/21656f024fb51467ca66d5c171782546d45d5f09))
* **canopen:** confirm a forced reset and keep a taken-over watch ([046e7a6](https://github.com/dborgards/CanKit.Pro/commit/046e7a625270ed15056c6f6ac602fe2b256ad673))
* **canopen:** copy a foreign PDO payload before the mapping upload ([3dffcfc](https://github.com/dborgards/CanKit.Pro/commit/3dffcfc1beaa3361456b2d987f29c839739f9604))
* **canopen:** cover the flying-master branches Codecov left partial ([b5fc8b2](https://github.com/dborgards/CanKit.Pro/commit/b5fc8b23a172966e54723721490b69f564ea6e15))
* **canopen:** do not let a dropped event keep two identical critical events apart ([67d7b3e](https://github.com/dborgards/CanKit.Pro/commit/67d7b3eb0e36a9d64281fcd959e5ec319b5330d2)), closes [#201](https://github.com/dborgards/CanKit.Pro/issues/201)
* **canopen:** drain the listen pump before a cancellation surfaces ([0115d1b](https://github.com/dborgards/CanKit.Pro/commit/0115d1bcf3fd210dd837299b62fdd3dc29ee8536))
* **canopen:** drop a reset or claim that was queued after dispose ([79abae9](https://github.com/dborgards/CanKit.Pro/commit/79abae93fac7d8fa31c40b42dfb3a287e145e29c))
* **canopen:** drop the extra catch around flying-master events ([ffa4a55](https://github.com/dborgards/CanKit.Pro/commit/ffa4a557d7f6fec9f838e20ccc0ac8d87ab48560))
* **canopen:** drop the poll gate again -- it is spent before the answer is known ([197bc23](https://github.com/dborgards/CanKit.Pro/commit/197bc236529649d3a3b5fbac7a6e1ce0d6ba1b48)), closes [#43](https://github.com/dborgards/CanKit.Pro/issues/43) [#43](https://github.com/dborgards/CanKit.Pro/issues/43) [#43](https://github.com/dborgards/CanKit.Pro/issues/43)
* **canopen:** drop the SDO self-guard again -- it could only subtract ([650c4ec](https://github.com/dborgards/CanKit.Pro/commit/650c4ecbdb226fe1033a176beb0d6b080c97a632))
* **canopen:** end the listen when its subscription ends early ([42280f8](https://github.com/dborgards/CanKit.Pro/commit/42280f814a7c535a928fd23605616e0f5c24f690))
* **canopen:** fail an SDO client transfer whose request is not sent ([326c837](https://github.com/dborgards/CanKit.Pro/commit/326c837f0bdb54dbf3f3df36f2b3d215675be63c))
* **canopen:** fill a reused 1016h slot before the count, write 1001h inside the EMCY chain, keep EMCY off the SYNC CAN-ID ([b42e585](https://github.com/dborgards/CanKit.Pro/commit/b42e585eb04ced2ada43b7d27e018157e280e807)), closes [#133](https://github.com/dborgards/CanKit.Pro/issues/133)
* **canopen:** filter heartbeat watches with Where ([8e4e496](https://github.com/dborgards/CanKit.Pro/commit/8e4e4967ae32fb1673819622fa0e3492f2d72983))
* **canopen:** fold an identical critical event only when nothing else about that producer came between ([f328b10](https://github.com/dborgards/CanKit.Pro/commit/f328b105e46f3fd2701bd944a60bba839f02305f)), closes [#201](https://github.com/dborgards/CanKit.Pro/issues/201)
* **canopen:** fold critical events again when the event that separated them is dropped ([ead89fc](https://github.com/dborgards/CanKit.Pro/commit/ead89fc2ea46915b9488cfdcf7f73b98d4614dd0)), closes [#201](https://github.com/dborgards/CanKit.Pro/issues/201)
* **canopen:** grow a sizeless SDO download geometrically ([a3ec1f5](https://github.com/dborgards/CanKit.Pro/commit/a3ec1f5cfc3382e7c67e8d65e936fd32079b2fd3))
* **canopen:** guard added sub-indices, leave synchronous TPDOs to the SYNC, actuate every RPDO on a shared COB-ID ([ba62a1e](https://github.com/dborgards/CanKit.Pro/commit/ba62a1e9a6a6907cf59d07da6ebc16125c2f8b00)), closes [#133](https://github.com/dborgards/CanKit.Pro/issues/133)
* **canopen:** hear echoed heartbeats and read 1000h once in the scan ([05d2a4c](https://github.com/dborgards/CanKit.Pro/commit/05d2a4cd03ee58e4e5ea6d5dfe9e2ebf2d58df35)), closes [#197](https://github.com/dborgards/CanKit.Pro/issues/197)
* **canopen:** hold boot commands while a forced reset is unconfirmed ([ae43e55](https://github.com/dborgards/CanKit.Pro/commit/ae43e557347af061fd1addb8514ea24dffe90c43))
* **canopen:** hold the write gate for a whole configuration and cap 1016h:00 at 127 ([7136443](https://github.com/dborgards/CanKit.Pro/commit/7136443157e368a2b341d4e380b3c1611fc813c6))
* **canopen:** ignore a stray command specifier during block SDO ([fa52f6e](https://github.com/dborgards/CanKit.Pro/commit/fa52f6e8bb736d0d44b04858befdfc810545d5b7))
* **canopen:** ignore SDO responses that name a different object ([#18](https://github.com/dborgards/CanKit.Pro/issues/18)) ([2c86298](https://github.com/dborgards/CanKit.Pro/commit/2c862983b033ef730f68a3f5738228e5acaec568))
* **canopen:** keep a producer's restart observable while guarding runs ([fc249c2](https://github.com/dborgards/CanKit.Pro/commit/fc249c2179f9ff8efc4374ab89ce8801d75ca6d5))
* **canopen:** keep boot-up and self-ignore through a detect cycle ([3a79b6f](https://github.com/dborgards/CanKit.Pro/commit/3a79b6f6e7ea9fd56ace38c3d8ef275bb6d8c3a7))
* **canopen:** keep dispatching after a flying-master subscriber throws ([fefeda7](https://github.com/dborgards/CanKit.Pro/commit/fefeda705a6f6aa43a76a9cde659632a975dd16c))
* **canopen:** keep SYNC and a PDO off one CAN-ID, run every reset subscriber, and chain the guarding reply ([5ac1edd](https://github.com/dborgards/CanKit.Pro/commit/5ac1eddbd1d5a2e92becab5d3bcadb9ec4208af4)), closes [#133](https://github.com/dborgards/CanKit.Pro/issues/133)
* **canopen:** keep the event pump running after a subscriber throws ([c0d82d1](https://github.com/dborgards/CanKit.Pro/commit/c0d82d10ce0be5eacb7dec3a0e5ac68056681704))
* **canopen:** keep the newest settings when a dropped separator lets two timeouts fold ([1238c0f](https://github.com/dborgards/CanKit.Pro/commit/1238c0f35e6efd387c6e4ea78ffa4f9a184877e7)), closes [#201](https://github.com/dborgards/CanKit.Pro/issues/201)
* **canopen:** keep the node's own SYNC reaching HandleSync ([5d02992](https://github.com/dborgards/CanKit.Pro/commit/5d02992a539cc4e994164df11eb078649ffe3ca7)), closes [#23](https://github.com/dborgards/CanKit.Pro/issues/23)
* **canopen:** keep the settings a timeout reports in its coalescing identity ([2224009](https://github.com/dborgards/CanKit.Pro/commit/2224009902c06ccaf5b6e6112e8ba60243b65379)), closes [#201](https://github.com/dborgards/CanKit.Pro/issues/201)
* **canopen:** keep timeout and EMCY events when the queue is full ([edfbc6d](https://github.com/dborgards/CanKit.Pro/commit/edfbc6d592662323b527f8cdc9d9787265a9df00))
* **canopen:** let a configured consumer outrank the heartbeat self-drop ([e2dc27f](https://github.com/dborgards/CanKit.Pro/commit/e2dc27f121364a034e816397aaaf4710870c66b4))
* **canopen:** let a pending confirmation, not the SDO timeout, decide ([11b20ff](https://github.com/dborgards/CanKit.Pro/commit/11b20ff119fa5d089f5adbbe2fad2354c449152d))
* **canopen:** let a timeout of each kind fold, and keep event-key equality out of the patch ([eaab440](https://github.com/dborgards/CanKit.Pro/commit/eaab44078bf5599d48d013bf5035076d7a7de922)), closes [#201](https://github.com/dborgards/CanKit.Pro/issues/201)
* **canopen:** let a timeout replace the one still waiting instead of keying it by settings ([9e2f275](https://github.com/dborgards/CanKit.Pro/commit/9e2f275bc0c912ef83e403ab8be393459edff33b)), closes [#201](https://github.com/dborgards/CanKit.Pro/issues/201)
* **canopen:** let only the latest SDO send decide, and fail on a cancel ([b59407f](https://github.com/dborgards/CanKit.Pro/commit/b59407f459829912afe7cffe5c2d82b3e1091d66))
* **canopen:** listen for heartbeats without opening a node ([306360d](https://github.com/dborgards/CanKit.Pro/commit/306360d913af4225fd93c7dbee27c0b8b8177663))
* **canopen:** look up described PDO objects by index ([513fdf1](https://github.com/dborgards/CanKit.Pro/commit/513fdf17bbfcec70ee45b033d8b981c3d386ee2f))
* **canopen:** make a dictionary write one transaction and re-arm event timers on Start ([b9ba86a](https://github.com/dborgards/CanKit.Pro/commit/b9ba86a84e25896feb6f57b7feb8e3272fe19356)), closes [#133](https://github.com/dborgards/CanKit.Pro/issues/133)
* **canopen:** never map an object of the communication profile area ([0aed9d1](https://github.com/dborgards/CanKit.Pro/commit/0aed9d145d3085aa4ec56bdf29863fd957c2eac8))
* **canopen:** no empty catch on a late send failure, exact scan output ([8a6fe82](https://github.com/dborgards/CanKit.Pro/commit/8a6fe82008d18272c7fa6455699a6a3cfc90760a)), closes [#197](https://github.com/dborgards/CanKit.Pro/issues/197)
* **canopen:** pause the active master while a forced reset is held ([7183668](https://github.com/dborgards/CanKit.Pro/commit/7183668daccfaa4532c06be301ca57ca56ef7619))
* **canopen:** put every dictionary mutation under the write gate and read an RTR on every TPDO sharing its COB-ID ([5a35616](https://github.com/dborgards/CanKit.Pro/commit/5a35616d730b0c95b66f68bd7d686dbb1e0bd0f9)), closes [#133](https://github.com/dborgards/CanKit.Pro/issues/133)
* **canopen:** raise the transfer's own failure for a thrown send ([af33fb3](https://github.com/dborgards/CanKit.Pro/commit/af33fb327af4e1e29cd034c6d2220d813b998817))
* **canopen:** re-arm the block-upload server deadline per sub-block ([#17](https://github.com/dborgards/CanKit.Pro/issues/17)) ([fd52d5d](https://github.com/dborgards/CanKit.Pro/commit/fd52d5d3e20abc7be26a97f449aadd9cb19d948a)), closes [#92](https://github.com/dborgards/CanKit.Pro/issues/92)
* **canopen:** refuse a truncated SDO segment instead of padding it with zeros ([e39b3c2](https://github.com/dborgards/CanKit.Pro/commit/e39b3c2a7f82d4af600f98b4763d20599866ce95)), closes [#203](https://github.com/dborgards/CanKit.Pro/issues/203)
* **canopen:** reject a peer DCF commissioned for another node ([ea5aff8](https://github.com/dborgards/CanKit.Pro/commit/ea5aff81beaeb17d8dac346f76978facea710d6b))
* **canopen:** release the listen window before a stream fault surfaces ([0033083](https://github.com/dborgards/CanKit.Pro/commit/00330839156cce4d1120169fa895dd4e2f72a5a2))
* **canopen:** release the SDO cancellation registration when the transfer ends ([#59](https://github.com/dborgards/CanKit.Pro/issues/59)) ([3c051bc](https://github.com/dborgards/CanKit.Pro/commit/3c051bc6dcd7cda11daed81c301b8952862b498a))
* **canopen:** remember a slave that checks in during a held force ([ba8733c](https://github.com/dborgards/CanKit.Pro/commit/ba8733c92e8f4d37cf67853ebd179d60d4674f32))
* **canopen:** remove SdoTransferMode.Expedited and .Segmented ([#97](https://github.com/dborgards/CanKit.Pro/issues/97)) ([a723b3c](https://github.com/dborgards/CanKit.Pro/commit/a723b3c408c9cf8f4223e9843c81a3e54324fa4e)), closes [#44](https://github.com/dborgards/CanKit.Pro/issues/44)
* **canopen:** report a $NODEID formula in a header entry as EdsParseException ([378cfd7](https://github.com/dborgards/CanKit.Pro/commit/378cfd7744f6d30ce8190c5662d00b1ff97c349f)), closes [dborgards/eds-dcf-net#577](https://github.com/dborgards/eds-dcf-net/issues/577) [#220](https://github.com/dborgards/CanKit.Pro/issues/220)
* **canopen:** report a cancelled SDO send on the background event too ([12c1d34](https://github.com/dborgards/CanKit.Pro/commit/12c1d3496af98e94d539bb0d759ff1e1dce48461))
* **canopen:** report a device type only for a four-byte 1000h answer ([b607135](https://github.com/dborgards/CanKit.Pro/commit/b60713570907732e5909e714ee6bb677e527da91))
* **canopen:** report adapter send failures from the NMT queue ([64e9053](https://github.com/dborgards/CanKit.Pro/commit/64e9053c445d1d447ba424607348733dd149fb82))
* **canopen:** require a confirmed cold reset and an honest snapshot ([88438fc](https://github.com/dborgards/CanKit.Pro/commit/88438fceb3ec072ed4f13e22a0058bee1e99b675))
* **canopen:** resolve a typed write's type under the write gate ([1a99e57](https://github.com/dborgards/CanKit.Pro/commit/1a99e573c913d9fe6bbee76c770e1f43a60e366d))
* **canopen:** restore only what has a power-on source, report unreadable PDO values, and take an orphan mapping record ([713370f](https://github.com/dborgards/CanKit.Pro/commit/713370fc46d945991874bb52a30c114dc9bf3c7d)), closes [#135](https://github.com/dborgards/CanKit.Pro/issues/135)
* **canopen:** reuse the 1016h slots an NMT reset hid when the consumer array grows again ([f9afa89](https://github.com/dborgards/CanKit.Pro/commit/f9afa89720f1cddc676730b932c5acc25e692154))
* **canopen:** serialize peer PDO reads and skip restricted COB-IDs ([3f1fca3](https://github.com/dborgards/CanKit.Pro/commit/3f1fca37cb9151f1f17818642c70d64877a6311f))
* **canopen:** start slaves that checked in during a failed force ([676f2bd](https://github.com/dborgards/CanKit.Pro/commit/676f2bd546e38ff8df7bf6229bd0806130227fe0))
* **canopen:** stop a block sub-block after its first unsent segment ([0d8bfa3](https://github.com/dborgards/CanKit.Pro/commit/0d8bfa3399fb412c31147fd2708e68401d7e4775))
* **canopen:** stop a node raising its own EMCY and heartbeat as a peer's ([d122d7c](https://github.com/dborgards/CanKit.Pro/commit/d122d7c62064a89c4e7c948ba9c135c7926a79cd)), closes [#95](https://github.com/dborgards/CanKit.Pro/issues/95) [#93](https://github.com/dborgards/CanKit.Pro/issues/93) [#103](https://github.com/dborgards/CanKit.Pro/issues/103) [#94](https://github.com/dborgards/CanKit.Pro/issues/94) [#95](https://github.com/dborgards/CanKit.Pro/issues/95)
* **canopen:** stop reading a boot-up frame as a node-guarding response ([14a60e3](https://github.com/dborgards/CanKit.Pro/commit/14a60e3d803524459cc8c1a92fd78e8fd4e08d52)), closes [#43](https://github.com/dborgards/CanKit.Pro/issues/43) [#114](https://github.com/dborgards/CanKit.Pro/issues/114) [#94](https://github.com/dborgards/CanKit.Pro/issues/94) [#43](https://github.com/dborgards/CanKit.Pro/issues/43) [#114](https://github.com/dborgards/CanKit.Pro/issues/114)
* **canopen:** store at the "save" write itself and see a change of state right after a direct configuration ([1f81641](https://github.com/dborgards/CanKit.Pro/commit/1f816414d08d4064226cf302872eed036450d5d9)), closes [#133](https://github.com/dborgards/CanKit.Pro/issues/133)
* **canopen:** treat an SDO initiate with e=0 as segmented ([0eaa3c1](https://github.com/dborgards/CanKit.Pro/commit/0eaa3c1ee6184cd05d43ecf65c874a96b8715c90))
* **canopen:** wrap only the $NODEID NotSupportedException, not every one ([fca278e](https://github.com/dborgards/CanKit.Pro/commit/fca278e4fa2f32c7c6b8fd95616d19fcb788a9e1))
* **canopen:** write a PDO configuration as one transaction on the actor loop ([0ba496b](https://github.com/dborgards/CanKit.Pro/commit/0ba496b3d1b2a7dc6aaa52baada6302231e8c0c3))
* **canopen:** yield an equal claim during the detect-cycle race ([0e7c34e](https://github.com/dborgards/CanKit.Pro/commit/0e7c34e08a79a7e00f4e07ddb378938e443e09e9))
* **isotp:** abandon a reception under way on dispose, keep tests net48-buildable ([7786a2b](https://github.com/dborgards/CanKit.Pro/commit/7786a2b9b2a601f77e45c6d8fdd5e01ee1fb92b7)), closes [#216](https://github.com/dborgards/CanKit.Pro/issues/216)
* **isotp:** an ignored First Frame leaves a reassembly in flight alone ([4e510ff](https://github.com/dborgards/CanKit.Pro/commit/4e510ff080741ab2445c5de7a037bcff49f04b61)), closes [#27](https://github.com/dborgards/CanKit.Pro/issues/27) [#56](https://github.com/dborgards/CanKit.Pro/issues/56)
* **isotp:** bound a functional collection by its frames' arrival stamps, and widen three functional tests' margins ([808e9c3](https://github.com/dborgards/CanKit.Pro/commit/808e9c3ef9fea7ab834593e460bac6568c772311))
* **isotp:** bound approximated SendConfirmed, make Dispose safe, guard DatagramReceived ([8b0fcb0](https://github.com/dborgards/CanKit.Pro/commit/8b0fcb01006533ca15cac4b671ca6ec1d19f8382)), closes [#202](https://github.com/dborgards/CanKit.Pro/issues/202) [#205](https://github.com/dborgards/CanKit.Pro/issues/205) [#206](https://github.com/dborgards/CanKit.Pro/issues/206)
* **isotp:** cut stale responses off at the last frame's handoff, not the first's ([c12294c](https://github.com/dborgards/CanKit.Pro/commit/c12294c0756a898f3dd2beacbf1b10107df6829a)), closes [#147](https://github.com/dborgards/CanKit.Pro/issues/147)
* **isotp:** drop what arrived before a discard unanswered, and reject a response that began before the request ([b7e054f](https://github.com/dborgards/CanKit.Pro/commit/b7e054f6faad3f52822b44efa4d20b11034188ad))
* **isotp:** ignore a First Frame that fits a Single Frame, take flow parameters once, withhold self-echoes, and discard inline on the actor ([495d959](https://github.com/dborgards/CanKit.Pro/commit/495d959611d7efe917744e457e1ff8a28e687a3f))
* **isotp:** keep a frame from after the deadline for the next collection, and report an ended subscription instead of returning empty at once ([2bb7a00](https://github.com/dborgards/CanKit.Pro/commit/2bb7a006d8db0cd3532f6120d5e09cb9fbf57d92)), closes [#150](https://github.com/dborgards/CanKit.Pro/issues/150)
* **isotp:** keep every First Frame read ahead of the actor in progress, not only the latest ([583a79e](https://github.com/dborgards/CanKit.Pro/commit/583a79e57513a8761cb62b7360555277edf8d774))
* **isotp:** keep the abort of a reception that began after a discard ([464b3cc](https://github.com/dborgards/CanKit.Pro/commit/464b3cc120a0b23f4dbd8dc288bb2d5b8d421dd1))
* **isotp:** opt subscriptions into echoes on a shared service ([be1685b](https://github.com/dborgards/CanKit.Pro/commit/be1685bae91d0f2bfd3539cc2fe02b8b4a6364bc))
* **isotp:** publish a First Frame on arrival, with its data, so a waiter sees it before the actor does ([a490e0f](https://github.com/dborgards/CanKit.Pro/commit/a490e0f244e401899e7f96065b9d0e75d228b7ab)), closes [#143](https://github.com/dborgards/CanKit.Pro/issues/143)
* **isotp:** pump the subscription on demand, so a buffered First Frame is seen before the reader task runs ([bcd56b1](https://github.com/dborgards/CanKit.Pro/commit/bcd56b19a5f171cc20c4633040df693e37bbde4e)), closes [#143](https://github.com/dborgards/CanKit.Pro/issues/143)
* **isotp:** read the listener's buffer against now, not against a negative window's past ([976fb04](https://github.com/dborgards/CanKit.Pro/commit/976fb046361adb3ffba5afbfcbf9430b7e11bd60))
* **isotp:** read the listener's buffer without a timer for a zero window ([e9ce1fc](https://github.com/dborgards/CanKit.Pro/commit/e9ce1fc79597aaba3e4f8bea7011b474559332b5)), closes [#150](https://github.com/dborgards/CanKit.Pro/issues/150)
* **isotp:** record the IIsoTpChannel member added in 7099e0a as the break it is ([5a21275](https://github.com/dborgards/CanKit.Pro/commit/5a2127516e25c63f5515c221745742bc59716468))
* **isotp:** refuse a listener window beyond a timer's reach before taking what was carried over ([92b2ae4](https://github.com/dborgards/CanKit.Pro/commit/92b2ae4ab857c9133e97aa214a1c92c69c359157))
* **isotp:** reject an escape-form First Frame whose FF_DL is not above 4095 ([94f7986](https://github.com/dborgards/CanKit.Pro/commit/94f79861635be6c587eed45b5a935e2a2d5ef593)), closes [#207](https://github.com/dborgards/CanKit.Pro/issues/207)
* **isotp:** reject zero-length First Frames in IsoTpFrameCodec ([52d0e95](https://github.com/dborgards/CanKit.Pro/commit/52d0e95de862ddcc8be72907497e99ef3c7c8187)), closes [#207](https://github.com/dborgards/CanKit.Pro/issues/207)
* **isotp:** report the first frame's handoff instant with a send, and cut stale responses off there ([19203ee](https://github.com/dborgards/CanKit.Pro/commit/19203ee47f2e80904c690165e15f022a870f5609))
* **isotp:** stamp a PDU when its last frame arrives, not when reassembly ends ([25e8146](https://github.com/dborgards/CanKit.Pro/commit/25e8146ba500d5d8f290d2be5ee79556b1ca11f9)), closes [#102](https://github.com/dborgards/CanKit.Pro/issues/102)
* **isotp:** stamp a received PDU with its first frame's arrival, and expose a reception in progress ([7099e0a](https://github.com/dborgards/CanKit.Pro/commit/7099e0a3272ff57f98822032edc288169b0b5c67))
* **isotp:** stop caller-side pumping once disposed and withdraw announcements last ([ca394c7](https://github.com/dborgards/CanKit.Pro/commit/ca394c7761fb071442bb26c8695cc390dea21ece)), closes [#216](https://github.com/dborgards/CanKit.Pro/issues/216)
* **isotp:** take the discard stamp and drain the subscription under the pump lock ([0cb0122](https://github.com/dborgards/CanKit.Pro/commit/0cb0122aeef929b3cf8fe990645cbed862cfa891))
* **isotp:** validate CAN_DL of received frames, bound reassembly by MaxReceivePduLength, bind the STmin timer to its transfer ([1e011f1](https://github.com/dborgards/CanKit.Pro/commit/1e011f102dde253febd86a1880086eac6c514df4)), closes [#27](https://github.com/dborgards/CanKit.Pro/issues/27) [#26](https://github.com/dborgards/CanKit.Pro/issues/26) [#25](https://github.com/dborgards/CanKit.Pro/issues/25) [#27](https://github.com/dborgards/CanKit.Pro/issues/27) [#26](https://github.com/dborgards/CanKit.Pro/issues/26)
* **isotp:** withdraw published receptions when the actor is already gone, cover the diff ([3fa4508](https://github.com/dborgards/CanKit.Pro/commit/3fa4508b8afc5c7002c922599652c76e7effb98c)), closes [#216](https://github.com/dborgards/CanKit.Pro/issues/216) [#94](https://github.com/dborgards/CanKit.Pro/issues/94)
* **isotp:** withhold echoes on one identifier only when the extension byte cannot tell the directions apart ([c747766](https://github.com/dborgards/CanKit.Pro/commit/c747766810f8df11ad11487cb8568f6be5f6424c))
* **j1939:** a peer claiming the vacated address ends the marker ([ca18105](https://github.com/dborgards/CanKit.Pro/commit/ca18105c20685d95798806696806c3654a801d46))
* **j1939:** answer a Request for Address Claimed with the Cannot Claim already waiting ([fe277da](https://github.com/dborgards/CanKit.Pro/commit/fe277dad5cef2f1b6c680be26c1e2655202ad746))
* **j1939:** answer a Request for Address Claimed, and re-scan the arbitrary field after losing a claimed address ([522b6ae](https://github.com/dborgards/CanKit.Pro/commit/522b6ae5700ac298ba7f5728dfb5d60c3bd7640a)), closes [#34](https://github.com/dborgards/CanKit.Pro/issues/34) [#35](https://github.com/dborgards/CanKit.Pro/issues/35) [#34](https://github.com/dborgards/CanKit.Pro/issues/34)
* **j1939:** back off before Cannot Claim and a re-claim, fault a second claim in arbitration, and send without a pool hop ([568dd5e](https://github.com/dborgards/CanKit.Pro/commit/568dd5e373870d0829c485b0ee1b1933f7f3b2ad)), closes [#58](https://github.com/dborgards/CanKit.Pro/issues/58) [#121](https://github.com/dborgards/CanKit.Pro/issues/121)
* **j1939:** bound the vacated marker to the claim it belongs to ([483324d](https://github.com/dborgards/CanKit.Pro/commit/483324d1a8891a4f6660d1a05c6d78f2cc336b1c))
* **j1939:** cannot-claim an equal NAME on the same address ([a4a2dd3](https://github.com/dborgards/CanKit.Pro/commit/a4a2dd3ee31a58517172e5da78e721d874696447))
* **j1939:** classify untabulated sub-byte SPN widths by terminal codes ([9b3b783](https://github.com/dborgards/CanKit.Pro/commit/9b3b783caccd70bf2803c22e55ab423016679a06)), closes [#99](https://github.com/dborgards/CanKit.Pro/issues/99)
* **j1939:** complete the Cannot Claim handoff for the claim that started it ([083fbca](https://github.com/dborgards/CanKit.Pro/commit/083fbcae3977aff52e2787fdd9018ab85dc12afb))
* **j1939:** consume the ledger before the source-address drop, and direct only to an address the node holds ([629c55f](https://github.com/dborgards/CanKit.Pro/commit/629c55f718f7b5dcde7e55415624a0e892c41751)), closes [#140](https://github.com/dborgards/CanKit.Pro/issues/140)
* **j1939:** fault a lost claim only after the Cannot Claim handoff ([b6c4ee6](https://github.com/dborgards/CanKit.Pro/commit/b6c4ee64eae1c4e90a2c22cd763a6def1c0be945))
* **j1939:** fault a lost claim only once its Cannot Claim is on the bus ([059c42d](https://github.com/dborgards/CanKit.Pro/commit/059c42d33c53c8011c169e03a2f863ca78a65775))
* **j1939:** filter live claim echoes with Where ([37dfb2d](https://github.com/dborgards/CanKit.Pro/commit/37dfb2d3a4f20a9532eafd30f9560fe0c365ec25))
* **j1939:** invalidate a lost address at once, keep the claim in hand through the backoff, and drop a Cannot Claim a new claim overtook ([8b1bf4c](https://github.com/dborgards/CanKit.Pro/commit/8b1bf4cef41257bfa7b57a0b324cc3e8d87296a0)), closes [#153](https://github.com/dborgards/CanKit.Pro/issues/153)
* **j1939:** keep a frame addressed to this node out of the self-drop ([33301e4](https://github.com/dborgards/CanKit.Pro/commit/33301e409e5dd1e79f66b05d0cc03dff5a0affcf))
* **j1939:** keep a timed-out claim echo from poisoning the retry ([ba413e6](https://github.com/dborgards/CanKit.Pro/commit/ba413e6374a199e5a5ae37928350fd39fb6ed7b2))
* **j1939:** keep claim-echo markers from outliving the send ([0c2606f](https://github.com/dborgards/CanKit.Pro/commit/0c2606f1ad79de617d5c55e64e32faa327dd3e51))
* **j1939:** keep the node's TP reader running after a reassembly abort ([13d18f2](https://github.com/dborgards/CanKit.Pro/commit/13d18f2a971ca9c3a567f258a25b84e5668d7d1c)), closes [#195](https://github.com/dborgards/CanKit.Pro/issues/195)
* **j1939:** keep the vacated address across further claim rounds ([654c4a7](https://github.com/dborgards/CanKit.Pro/commit/654c4a7523b9ba1ce761cb6892d7c1a05fb8c287))
* **j1939:** read and write the NAME of an address claim in wire order ([fa832ba](https://github.com/dborgards/CanKit.Pro/commit/fa832bae0da64220147ad8f9ec329d36875a74a9))
* **j1939:** recognise the address being vacated while a claim is in flight ([0a29766](https://github.com/dborgards/CanKit.Pro/commit/0a297666f584d0dea2802f05db673c598c989008))
* **j1939:** recognise the node's own echo by content, not by the address it vacated ([79e85bb](https://github.com/dborgards/CanKit.Pro/commit/79e85bbf577618e81d7c8e940550a75bd46ef4ed)), closes [#119](https://github.com/dborgards/CanKit.Pro/issues/119) [#121](https://github.com/dborgards/CanKit.Pro/issues/121) [#119](https://github.com/dborgards/CanKit.Pro/issues/119)
* **j1939:** report SPN indicator ranges instead of scaling them ([#98](https://github.com/dborgards/CanKit.Pro/issues/98)) ([e6afc0a](https://github.com/dborgards/CanKit.Pro/commit/e6afc0a0bd072e60dde9265446123df36f3bd892)), closes [#99](https://github.com/dborgards/CanKit.Pro/issues/99) [#37](https://github.com/dborgards/CanKit.Pro/issues/37) [#99](https://github.com/dborgards/CanKit.Pro/issues/99) [#97](https://github.com/dborgards/CanKit.Pro/issues/97)
* **j1939:** settle every Cannot Claim still in flight when the node is disposed ([7550d3d](https://github.com/dborgards/CanKit.Pro/commit/7550d3d30a0ce67607c085f3b18191db86800a09))
* **j1939:** stop a node raising its own broadcasts as peer traffic ([f5cf268](https://github.com/dborgards/CanKit.Pro/commit/f5cf2689b4042c47018ecbb7197f80191a98b796)), closes [#95](https://github.com/dborgards/CanKit.Pro/issues/95) [#95](https://github.com/dborgards/CanKit.Pro/issues/95) [#94](https://github.com/dborgards/CanKit.Pro/issues/94) [#94](https://github.com/dborgards/CanKit.Pro/issues/94) [#94](https://github.com/dborgards/CanKit.Pro/issues/94)
* **j1939:** take the claim backoff from the low byte of the NAME's sum, and read the lost address in the handler ([35c8799](https://github.com/dborgards/CanKit.Pro/commit/35c879935d8d51397a280ef17b70f6db7ed0f3c8))
* **j1939:** tie a delayed Cannot Claim to its loss, and start a backing-off round on a request or a contest ([99cc809](https://github.com/dborgards/CanKit.Pro/commit/99cc8099d28c8efe1c6fbb9559454c5ab0c75bb1))
* **j1939tp:** answer an already-canceled send with a cancellation ([8155baf](https://github.com/dborgards/CanKit.Pro/commit/8155baffce94e343c04ee4117f9973b1992b5928))
* **j1939tp:** apply a retransmit request stashed mid-block as soon as the outstanding packet is confirmed ([eb331b6](https://github.com/dborgards/CanKit.Pro/commit/eb331b66fb4fbb98314ff227dea34c22618aa661))
* **j1939tp:** check the destination of every TP.CM control frame ([#30](https://github.com/dborgards/CanKit.Pro/issues/30)) ([7eeb4b1](https://github.com/dborgards/CanKit.Pro/commit/7eeb4b1ea92755551131fd50d378e0efbf5d01d6))
* **j1939tp:** classify packet 0 and any repeated packet by table 7 ([780ace3](https://github.com/dborgards/CanKit.Pro/commit/780ace30d91dfe76d55a8303107b17c630d86f79)), closes [#145](https://github.com/dborgards/CanKit.Pro/issues/145)
* **j1939tp:** count only packets up to the outstanding one as sent while a block drains ([ebb24b3](https://github.com/dborgards/CanKit.Pro/commit/ebb24b3dee4236a91671ed90cff11ed45a339dfe))
* **j1939tp:** fault the previous BAM when a new one arrives ([e48a1c8](https://github.com/dborgards/CanKit.Pro/commit/e48a1c8ee52b620376e2abe6a962107b98c97cc8)), closes [#169](https://github.com/dborgards/CanKit.Pro/issues/169)
* **j1939tp:** hold the CTS-to-first-DT window to T2, send table 7's abort codes, release the send's token registration ([8a21aa8](https://github.com/dborgards/CanKit.Pro/commit/8a21aa8afc95e6e3d02db67c8674f2d9c8facb34))
* **j1939tp:** keep the duplicate-send error when the destination is full ([f6c4de5](https://github.com/dborgards/CanKit.Pro/commit/f6c4de539e82304a097373d6aebbf5e09a413857))
* **j1939tp:** keep the source-address self-check behind the echo gate ([15aaf1c](https://github.com/dborgards/CanKit.Pro/commit/15aaf1c77deaf6c9f6c3aa3c1cd02c5a94d831c7)), closes [#93](https://github.com/dborgards/CanKit.Pro/issues/93)
* **j1939tp:** publish a send's outcome and release its slot in one step ([8924562](https://github.com/dborgards/CanKit.Pro/commit/8924562dca4ffaf5a4ab311145aeeb7f12d722ad))
* **j1939tp:** read a retransmit request past the byte wrap, accept EndOfMsgAck after a partial retransmit, and state the event's threading ([ff02cc1](https://github.com/dborgards/CanKit.Pro/commit/ff02cc1a0800837e61876f7bf61b154b74fd4f0d))
* **j1939tp:** reserve the queue slot before copying the payload ([eec0bbb](https://github.com/dborgards/CanKit.Pro/commit/eec0bbbc6ff8cfe407a704227cc37e1beab47759))
* **j1939tp:** reserve the send limit at admission, before the actor is posted ([97939a5](https://github.com/dborgards/CanKit.Pro/commit/97939a5b98847444070553aaec075d6836d2d567))
* **j1939tp:** roll back the admission slot on every failure and drop dead queue code ([3b8efe1](https://github.com/dborgards/CanKit.Pro/commit/3b8efe10ebe2ed02cd451f834b313b3378aea993))
* **j1939tp:** send nothing outside table 7 ([38c0461](https://github.com/dborgards/CanKit.Pro/commit/38c04615097d9f987c16ba25cfe1e4db18ff0cf8))
* **j1939tp:** send to one destination at a time instead of interleaving sessions ([#32](https://github.com/dborgards/CanKit.Pro/issues/32)) ([0b5a2ad](https://github.com/dborgards/CanKit.Pro/commit/0b5a2add0e9a96ce07a302395bc85b7475b4e414))
* **j1939tp:** skip queued sends that were completed before their turn ([cacec4c](https://github.com/dborgards/CanKit.Pro/commit/cacec4cbbb54ce204440712cc951ecd0a865c1d7))
* **j1939tp:** tell a retransmit request from the next block by the highest packet ever sent ([b32ca2b](https://github.com/dborgards/CanKit.Pro/commit/b32ca2ba30017f0246c608ee920c946acaa46edb))
* **j1939tp:** unlink queued sends in O(1) instead of shifting a list ([57115ae](https://github.com/dborgards/CanKit.Pro/commit/57115ae19dd63e1ea8cb86083294a82b5062beee))
* **rawcan:** cancel the abandoned transmit on timeout, guard test disposal ([ca43189](https://github.com/dborgards/CanKit.Pro/commit/ca4318974287ff8862d6050c6fd42b9162866c42)), closes [#216](https://github.com/dborgards/CanKit.Pro/issues/216)
* **rawcan:** claim a pending send by completing it, not by asking whether it is complete ([f75b40f](https://github.com/dborgards/CanKit.Pro/commit/f75b40fd58f2efe728983bf6da8f5dc703e9d39c)), closes [#92](https://github.com/dborgards/CanKit.Pro/issues/92)
* **rawcan:** compare CanFrameEvent by payload bytes, and correct the stale narrative ([5f2a0e1](https://github.com/dborgards/CanKit.Pro/commit/5f2a0e1e3f65e492bbb50949806f3b65ab1481c0))
* **rawcan:** hand the driver a private frame copy, honour caller cancellation, fix Dispose races ([163b41b](https://github.com/dborgards/CanKit.Pro/commit/163b41b921e5f470f24e1c642cdb24e9706944d6))
* **rawcan:** reject CanIdFilter ranges and masks outside their ID space ([45e10c7](https://github.com/dborgards/CanKit.Pro/commit/45e10c72c580ffeb1995c5997298bb1683552374)), closes [#53](https://github.com/dborgards/CanKit.Pro/issues/53) [#53](https://github.com/dborgards/CanKit.Pro/issues/53)
* **rawcan:** reject undefined CanFilterIDType in CanIdFilter ([c20f361](https://github.com/dborgards/CanKit.Pro/commit/c20f361b8cc2b6abccb67066500a6423b932bc46))
* **rawcan:** report the instant the driver accepted a frame ([d75876e](https://github.com/dborgards/CanKit.Pro/commit/d75876e028239f29b6c334f02055a5b20f010b85)), closes [#92](https://github.com/dborgards/CanKit.Pro/issues/92)
* **rawcan:** stamp the approximated send at driver completion, not at resumption ([2c7fa4f](https://github.com/dborgards/CanKit.Pro/commit/2c7fa4fedd0cf34779d2bdbe80fb9737d5e357f3)), closes [#92](https://github.com/dborgards/CanKit.Pro/issues/92)
* **rawcan:** stop a cancellation from waiting on an unrelated send's driver call ([2a87013](https://github.com/dborgards/CanKit.Pro/commit/2a870132db545593914e2f03e423c3860e0942f1)), closes [#92](https://github.com/dborgards/CanKit.Pro/issues/92) [#24](https://github.com/dborgards/CanKit.Pro/issues/24)
* **rawcan:** stop an expired pending send from poisoning the echo FIFO ([cf24dc0](https://github.com/dborgards/CanKit.Pro/commit/cf24dc083a849d7e5aa448b4daf697719717272d)), closes [#24](https://github.com/dborgards/CanKit.Pro/issues/24)
* **rawcan:** take the arrival stamp before the pending-send lock ([fc68498](https://github.com/dborgards/CanKit.Pro/commit/fc6849885b6266b060564e590b15abe306010d68)), closes [#92](https://github.com/dborgards/CanKit.Pro/issues/92)
* **rawcan:** the echo flag is host-scoped, so protocol layers opt in ([30eed7c](https://github.com/dborgards/CanKit.Pro/commit/30eed7cdaea645a1a7dfe991d6ee8eda0d3970f1)), closes [#23](https://github.com/dborgards/CanKit.Pro/issues/23)
* **reliability:** coalesce BusStateMonitor's error-frame rechecks ([a68994e](https://github.com/dborgards/CanKit.Pro/commit/a68994e4d42a7da97729735bc1a9924483ab4d19)), closes [#22](https://github.com/dborgards/CanKit.Pro/issues/22)
* **test:** keep twenty gaps after trimming the warm-up, not nineteen ([dc47793](https://github.com/dborgards/CanKit.Pro/commit/dc47793004f22e14732045622efd23cb9172a3ea))
* **test:** size the periodic rate bound by its error term instead of picking a st…
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.

CANopen: SdoTransferMode.Expedited/Segmented are accepted but not enforced

3 participants