Changes worth sending upstream to CanKit¶
CanKit.Pro consumes CanKit from nuget.org and deliberately does not fork it. Its predecessor, CanKit.Pro.legacy, was a fork, and it improved CanKit itself along the way. Those improvements stayed behind under Apache-2.0 when the product moved here, which is correct — but it leaves them stranded in a fork nobody consumes.
This page lists them so they can be offered upstream as pull requests rather than quietly lost. None of them is required for CanKit.Pro to build today; the first is the only one CanKit.Pro actively works around. §7 is one it guards against instead: the CiA 801 bit-rate detection refuses the adapter it names.
1. Protocol error codes (worked around here)¶
CanKitErrorCode reserves the 6000 range for transport and protocol errors and defines
TransportOperationFailed = 6001. The fork added four more, and CanKit.Pro's ISO-TP, J1939-TP,
CANopen, J1939 and UDS layers all report them:
ProtocolTimeout = 6002 // ISO-TP N_As/N_Bs/N_Cr, J1939-TP session timeout, UDS P2/P2*
ProtocolPeerAbort = 6003 // ISO-TP Overflow/WFTmax, J1939-TP Abort, CANopen SDO abort
ProtocolNegativeResponse = 6004 // UDS 0x7F
AddressClaimFailed = 6005 // J1939 address claim lost with no arbitrary address available
Until they exist upstream, CanKit.Pro.RawCan.ProtocolErrorCodes declares them as constants of the
enum type with these exact values, so the numbers callers observe are already the ones an upstream
adoption would produce. Adopting them upstream makes that file redundant and nothing else changes.
These codes are useful beyond CanKit.Pro: any transport or protocol layer built on CanKit needs to distinguish "the peer aborted" from "we timed out" from "the driver refused the frame".
2. Frame ownership and lifetime¶
The fork made the frame-ownership contract explicit and enforced it (arc42 §8.1, ADR-9):
CanFrame.Duplicate(IBufferAllocator)— an independent copy over a freshly rented buffer, so one consumer's disposal cannot invalidate another's frame.CanFrame.Dispose()honouringOwnMemoryinstead of disposing unconditionally.VirtualBusHub.Broadcasthanding each recipient its own copy, andVirtualBus.InternalDeliverdisposing frames it drops on a filter miss rather than leaking their pooled buffers.
This is the difference between "multiple consumers per bus happens to work" and "multiple consumers per bus is a contract", which is what any demultiplexing layer needs.
3. Loopback adapter fidelity¶
CanKit.Adapter.VirtualdeclaringCanFeature.EchoinStaticFeatures. It genuinely has the capability throughChannelWorkMode.Echo; not declaring it means capability-gated echo behaviour cannot be exercised on hardware-free CI at all.- Marking its self-echo with
IsEcho = true, so consumers can tell an echo from bus traffic — without it, any TX-confirm implementation on top of CanKit silently cannot match echoes. CanKit.Pro keeps a second, unflagged echo world in the tests (EchoWorld.Unflagged) because the published adapter still leaves the flag false; protocol self-traffic guards are run in both worlds until that changes. VirtualBusHubregistry rework: atomic join, and removing a hub when its last member leaves (the registry otherwise grows without bound across sessions).
4. Registry thread safety¶
CanRegistry was reworked to be safe under parallel late registration while readers call
TryOpenEndPoint/EnumerateEndPoints on the shared singleton (NFR-008), and
RegisterEndPoint/RawEndpointRegistration grew alongside it. Its stress test —
RawCanConcurrencyTests.CanRegistry_Parallel_Registration_And_Readers_Do_Not_Race — stayed in the
legacy repository rather than moving here: it drives CanKit's registry through internals the
published package does not expose, so it is a test of CanKit. It should travel with this change.
CanKit.Pro kept the half of that file it actually owns, the demultiplex-service stress test.
5. Assorted core and adapter fixes¶
IPeriodicTx.Faulted, so a periodic transmit that starts failing is observable rather than silently stopping.AsyncFramePipe,SoftwarePeriodicTxandPreciseDelayrobustness work, including falling back to a coarse sleep whenclock_nanosleepkeeps failing.- SocketCAN: treating
errno 0as a missingctrlmodeon open, and aligning BCM operations with the Linux API. - ZLG and ControlCAN: freeing the auto-send index when a
PeriodicTxconstructor throws.
6. Echo behaviour that does not match the contract (measured, #249)¶
Evidence: reviews/2026-10-06-echo-hardware-measurements.md.
- Kvaser (
KvaserBus.cs, notify mask; filed as pkuyo/CanKit#70): the callback is registered withcanNOTIFY_RX(andcanNOTIFY_ERROR) only, althoughcanIOCTL_SET_TXACKis switched on. TX acknowledgements are reported withcanNOTIFY_TX(0x2), whichCanlib.csdoes not define. Measured on a Leaf Light v2: with mask 0x3 three callbacks for three sends, with 0x1 none; in Echo mode a quiet bus delivers no own frame at all. Proposal: definecanNOTIFY_TXand add it to the mask when the channel is in Echo mode. - PCAN (
PcanBus.cs,PcanClassicTransceiver.cs; filed as pkuyo/CanKit#71): in Echo mode (AllowEchoFrames) the own frames arrive but withoutIsEchoset. Measured on a PCAN USB Pro FD, classic and FD. Proposal: setIsEchofor frames received with the self-receive message type. - Vector (not measured, from source): Classic drops its own TX event in Echo mode
(
if (recEcho && isEcho) return) and delivers it flagged in Normal mode; FD never delivers it. Needs a measurement on hardware before it is reported.
7. Kvaser never applies listen-only (read, #345)¶
From source, not measured; not filed upstream yet. KvaserProvider.QueryCapabilities adds
CanFeature.ListenOnly when the channel reports canCHANNEL_CAP_SILENT_MODE, and the
configurator stores ChannelWorkMode.ListenOnly, but nothing in CanKit.Adapter.Kvaser @ v0.5.6
calls canSetBusOutputControl: KvaserBus.ApplyConfig handles only ChannelWorkMode.Echo. The
bus therefore reports listen-only while the controller runs in CANlib's default output mode —
normal, from memory, not measured — and so acknowledges frames and sends error flags. PCAN (PcanParameter.ListenOnly), Vector
(XL_OUTPUT_MODE_SILENT, but only with init access: without it VectorBus skips ApplyConfig
and logs a warning), SocketCAN (the listen-only ctrlmode, on a netlink endpoint), ZLG and
ControlCAN (the mode byte of the channel configuration) do apply it.
Proposal: before canBusOn, call canSetBusOutputControl(handle, canDRIVER_SILENT) when
WorkMode == ListenOnly and canDRIVER_NORMAL otherwise. Until the CanKit version CanKit.Pro is
built against does that, LssBitRateDetection refuses a Kvaser bus while RequireListenOnly
holds, since its listen-only guarantee (CiA 801 §5.2.1) would otherwise be false on that adapter.
How to use this list¶
Each item is a self-contained change with its own commits in the legacy repository's develop
branch. Whoever opens the pull requests should take the commits from there rather than re-deriving
them — the tests came with them.