Repository navigation
fix(j1939)!: report SPN indicator ranges instead of scaling them - #98
Conversation
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
PR SummaryMedium Risk Overview Decoding adds Docs, the J1939 quickstart sample, FR-J1939-002 in the SRS, and the public API approval baseline are updated. Reviewed by Cursor Bugbot for commit 2ac4e6d. Bugbot is set up for automated code reviews on this repo. Configure here. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f1f7c61a6a
ℹ️ 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".
… 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
CI:
|
There was a problem hiding this comment.
🔵 Needs a closer look
The breaking API and signed-value behavior warrant human approval, with signed-mode documentation nits still unresolved.
Review details
Suppressed comments (4)
src/CanKit.Pro.J1939/J1939SpnValue.cs:84
- The
IsNotAvailableXML summary repeats the same unsigned-only rule (all bits set), butJ1939Spn.Classify(..., isSigned: true)intentionally returns this property for the signed leading-0x7F indicator range. Update the property documentation to describe the signed encoding as well.
/// <summary>True when the sending ECU does not have this parameter (all bits set).</summary>
src/CanKit.Pro.J1939/J1939SpnValue.cs:29
- This summary documents only the unsigned parameter-specific code (
0xFB), although signed SPNs use the corresponding0x7Bleading-byte range and return the same enum member. Without that distinction the public API documentation is inconsistent with the signed behavior added here.
/// <summary>
/// The "parameter specific indicator" code (<c>0xFB</c> leading byte): its meaning is defined
/// by the individual SPN, not by J1939-71, so it cannot be scaled into a physical value here.
/// </summary>
ParameterSpecific = 2,
src/CanKit.Pro.J1939/J1939SpnValue.cs:35
- The reserved-range summary is also unsigned-only (
0xFC..0xFD), while signed classification reports0x7C..0x7Din the sameReservedstate. Please include the signed encoding in this public API documentation.
/// <summary>
/// Reserved for future indicator bits (<c>0xFC</c>..<c>0xFD</c> leading byte). Not a
/// measurement.
/// </summary>
Reserved = 3,
src/CanKit.Pro.J1939/J1939SpnValue.cs:41
- This error summary names only the unsigned
0xFEleading byte, but signed SPNs report0x7EasError. The documentation should cover both forms just asClassifydoes.
/// <summary>
/// The transmitting ECU has the parameter but detected an error in it (<c>0xFE</c> leading
/// byte).
/// </summary>
Error = 4,
- Files reviewed: 13/13 changed files
- Comments generated: 2
- Review effort level: Lite
…N 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
… 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
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
## [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…
What does this change?
SPN extraction applied
physical = raw × resolution + offsetto every bit pattern, including the SAE J1939-71 §5.1.1 indicator ranges that sit at the top of every SPN's raw range. An EEC1 frame from an ECU that does not have SPN 190 therefore reported 8191.875 rpm rather than "no reading" — a plausible, wrong measurement, which is the worst failure mode for diagnostic data. Extraction now returns aJ1939SpnValuethat says which of the two it found, and signed SPNs are supported.Closes #37.
The shape
Kind—Valid,NotAvailable,Error,Reserved,ParameterSpecific.NotAvailableis0so a default-initialised value does not read as a valid zero.Raw— the bit pattern as read off the wire, indicator or not, so the exact code can still be logged or forwarded.ValuethrowsInvalidOperationExceptionunless the field carries a measurement;TryGetValueandGetValueOrDefault(NaN by default) are the non-throwing routes.J1939Spn.Classifyexposes the range check on its own;J1939Spn.FromRawscales an already-extracted raw.J1939SpnDefinition.IsSigned, anisSignedparameter onExtract, andExtractRawSigned. For a signed parameter the indicator codes sit at the top of the signed range (0x7B..0x7F), so0xFFFFis an ordinary −1, not "not available".Modelled on the existing
TxConfirmationidiom (areadonly record structplus a reason enum) rather than a new pattern.Why the signature changed rather than gaining a sibling
ADR 0001 and the owner's decision on #37: until the
v1.3.0tag, breaking API changes are allowed and map to a minor bump, and no[Obsolete]shim may be introduced to dodge a break. ATryGetSpnValuebeside aGetSpnValuethat still reported0xFFFFas 8191.875 rpm would have left the defect reachable through the API that is easiest to call.src/still contains no[Obsolete]member.What review changed
1. The inferred sub-byte widths are now pinned, and the open question is filed rather than settled here. §5.1.1 tabulates 8/16/32-bit, 4-bit and 2-bit fields.
Classifygeneralises to the rest by keeping the leading group a fixed size per width class, which means the indicator fraction of the range is whatever the tabulated width of that class already spends. Those inferred widths (3, 5, 6, 7) were the only ones with no test — exactly backwards, since a reading the standard does not state is the one that can drift unnoticed. Twenty-six known-answer cases now pin them.The consequence worth a second opinion is that a 3-bit field spends half its value space on indicators. Codex proposed the alternative reading (terminal codes only, so 5-bit would be 27..31). I have not adopted it here and the reasoning is on the thread: the standard preserves the fraction across widths — a 16-bit SPN spends 256 raw values on "not available", not one — and the narrower reading fails in the dangerous direction, scaling an indicator into a plausible measurement, which is the defect #37 exists to fix. Filed as #99 with all four candidate readings and this comparison, because changing a public classifier belongs in its own PR, not the one that introduced it.
2. Coverage. The five patch lines Codecov flagged were all real gaps.
TryGetValueandGetValueOrDefaultwere asserted only on the indicator side — their measurement side, the path almost every caller takes, was unpinned.FromIndicator's out-of-range guard was never reached.J1939SpnDefinition.Name/Pgn/Unithad no assertion at all, so a catalog entry could have carried the wrong PGN or unit with every decode test passing. Both files are now 100 % line and branch, measured rather than assumed.3. The signed encoding was documented backwards in places. Every
J1939SpnValueKindsummary described only the unsigned codes, althoughClassifyreturns the same members for signed SLOTs at the sign-bit-clear codes — so a consumer branching onIsNotAvailableand trusting the doc would have treated-1as "no reading". Worse, the comment on the signed catalog test stated the rule inverted, directly above an assertion saying the opposite. Both fixed, and the corrected rule is now pinned by an assertion (signed0xFFFF→-0.1 deg) rather than only described.Interpretation notes
Classifyand tracked in J1939 SPN: a 3-bit field spends half its value space on indicators #99.Valid: J1939-71 defines no indicator codes there, and inventing some would report real measurements as missing.Docs updated
src/CanKit.Pro.J1939/README.md(rendered intodocs/packages/j1939.md), thedocs/index.mdquickstart, the J1939 quickstart sample, and the FR-J1939-002 row of the SRS — which previously required only scale/offset extraction and now requires the indicator ranges to be reported as such, with the verification column naming the known-answer tests.Type of change
feat— new behaviour (minor release)fix/perf— bug or performance fix (patch release)docs/test/refactor/chore/ci— no release!in the title, plus aBREAKING CHANGE:footer explaining the migration)The
BREAKING CHANGE:footer is on commitf1f7c61, in the commit message — this repository merges rather than squashes, so the analyser reads commits and never reads this body. Per ADR 0001 §7 breaking maps to minor for the duration of the pre-1.3.0 window, so this does not publish 2.0.0.Checklist
dotnet build CanKit.Pro.sln -c Releasesucceeds — run as-p:CI=true(warnings as errors): 0 warnings, 0 errorsdotnet test CanKit.Pro.sln -c Releasepasses —--no-build --framework net10.0: 583 passed, 0 failed (140 of them underJ1939Spn*), after mergingmainata723b3cAlso run:
dotnet format CanKit.Pro.sln --verify-no-changes— clean (exit 0). The net48 leg was not run; it is Windows-only in this checkout.Tests
tests/CanKit.Pro.Tests/TestCases/J1939/J1939SpnIndicatorTests.cspins both boundaries of every band, so an off-by-one classifier fails:0x00,0xFA0xFB0xFC,0xFD0xFE0xFF0x0000,0xFAFF0xFB00,0xFBFF0xFC00,0xFDFF0xFE00,0xFEFF0xFF00,0xFFFF0x00000000,0xFAFFFFFF0xFB000000,0xFBFFFFFF0xFC000000,0xFDFFFFFF0xFE000000,0xFEFFFFFF0xFF000000,0xFFFFFFFF0x0,0xA0xB0xC,0xD0xE0xF0b00,0b010b100b110,10..34,56,70x7AFF,0x8000,0xFFFF0x7B000x7C00,0x7DFF0x7E00,0x7EFF0x7F00,0x7FFFPlus: the 12- and 64-bit generalisation, the regression itself (
0xFFFFat 0.125 rpm/bit is not 8191.875), the top of the valid band still scaling (0xFAFF→ 8031.875 rpm) and the very next raw count flipping to an indicator, signed sign-extension andExtractRawSigned,Classifyrejecting out-of-range bit lengths, the value type's default/factories/ToString, both non-throwing accessors on the measurement side,FromIndicator's out-of-range guard, and a catalog definition's descriptive members.API approval baseline
tests/CanKit.Pro.Tests/ApiApprovals/CanKit.Pro.J1939.approved.txtwas replaced with the test's own generated.received.txt, not hand-written. LikeTxConfirmation, the newreadonly record structrenders aspublic readonly struct … : IEquatable<>— a PublicApiGenerator limitation, matching the existing precedent inCanKit.Pro.RawCan.approved.txt.A note on CI
macos-latestwent red once on an earlier head withJ1939TpTests.Parallel_Bam_And_TwoCm_Sessions_Do_Not_Interfere, a wall-clock T1 timeout in code this diff does not touch. It passed on a re-run of the identical commit. That is the #92 population — six distinct tests in about an hour, none repeating, all on branches whose diffs cannot reach the failing code.🤖 Generated with Claude Code
https://claude.ai/code/session_011Zd6AyAtcZApgfRC2Rkitj