Skip to content

ICanBusService Interface

One demultiplexing service instance per ICanBus: it turns the single RX stream exposed by ICanBus.FrameObserved into N independent, filtered read-only ISubscriptions, so multiple protocol instances (ISO-TP, J1939, CANopen, …) can each see their own view of the bus without competing over ICanBus.ReceiveAsync(Int32, Int32, CancellationToken) (arc42 §5.3, ADR-5; FR-RAW-010..013).

public interface ICanBusService : System.IDisposable

Derived
↳ CanBusService

Implements IDisposable

Remarks

The service is built purely on top of the public ICanBus.FrameObserved surface (a read-only CanFrameView per frame, with no disposal/ownership concerns), so it works identically for every adapter with no per-adapter changes. Disposing the service unwinds all outstanding subscriptions and detaches its handler from the underlying bus (FR-RAW-012). Dispose is idempotent.

Echoes. When the bus is configured for echo (WorkMode == ChannelWorkMode.Echo) it reports the host's own transmissions back through the same RX stream, flagged as such. A subscription receives them only if it asked for them, and the flag rides along on every delivered CanFrameEvent either way. The default is off because the frames a protocol layer sends are not frames it received: a J1939 node that treats its own Address Claim as a competitor's, or a CANopen node that acts on its own PDO, is broken in a way that only shows up on hardware that happens to echo. Before this flag existed each of those layers each carried their own answer to "is this mine?" — comparing NAMEs, comparing source addresses, guarding on actor affinity. Those checks are still there and still needed; what changed is that the flag and the timestamp are now available to a caller who wants them.

Two limits, both of which make the gate a convenience rather than a guarantee. First, it can only drop what the adapter flags: an adapter that echoes without setting IsEcho — CanKit.Adapter.Virtual in ChannelWorkMode.Echo does — delivers its echo to every subscription regardless of includeEcho. Second, and more important, the flag is host-scoped: it says something on this host transmitted the frame, not which of the possibly several protocol instances sharing this service did. A sibling instance's traffic is flagged identically to one's own.

So includeEcho: false suits a single consumer that owns its bus. A protocol layer that may share a service — every one in this repository does, by documented design — asks for echoes instead, and must then tell its own traffic apart by something it actually owns. That is not a workaround for a missing feature: the demux genuinely cannot attribute a transmission to a local instance.

How far each layer takes that is a per-layer decision, not a guarantee this interface makes. The J1939 transport rejects its own source address; the J1939 node recognises an Address Claim it transmitted when the bus echoes one, and treats another CA's claim — the same NAME included — as a contest; it rejects its own source address on an application PGN. CANopen rejects its own node id on an EMCY or a heartbeat.

None is a blanket self-filter, and each exception is deliberate. A J1939 frame addressed to the node itself is still delivered, so a request against one's own address is answered. CANopen still delivers NMT, SYNC, both SDO directions and RPDOs from its own producer — a node acts on its own SYNC, and an RPDO's COB-ID is whatever the application configured — and still feeds a heartbeat or node-guarding consumer registered for the local node id. The shared rule is that an explicitly configured or explicitly addressed frame outranks a guess about who sent it.

Properties

ICanBusService.Bus Property

The underlying bus this service demultiplexes.

CanKit.Abstractions.API.Can.ICanBus Bus { get; }

Property Value

ICanBus

ICanBusService.SubscriptionCount Property

Number of currently registered (not yet disposed) subscriptions. Primarily for diagnostics/tests: after disposing every subscription it returns to zero, proving no registry entries leak (FR-RAW-012).

int SubscriptionCount { get; }

Property Value

Int32

Methods

ICanBusService.FindOverlappingFilterSubscriptions() Method

Diagnostic: finds every pair of currently registered, still-undisposed CanIdFilter-based subscriptions whose ID spaces overlap, and the range of CAN IDs each pair shares (FR-RAW-041, "Should") -- helps catch misconfiguration when multiple protocol instances were meant to have disjoint ID ranges but don't. Subscriptions registered via the generic Subscribe(Func<CanFrameEvent,bool>, Nullable<int>, bool) predicate overload are opaque and are not analyzable, so they are skipped.

System.Collections.Generic.IReadOnlyList<CanKit.Pro.RawCan.FilterOverlap> FindOverlappingFilterSubscriptions();

Returns

IReadOnlyList<FilterOverlap>
One FilterOverlap per overlapping pair, each naming the two subscriptions and the ID range on which they collide. Empty when no two filters share ID space.

ICanBusService.SendConfirmedAsync(CanFrame, Nullable<TimeSpan>, CancellationToken) Method

Sends frame and asynchronously confirms it was actually sent, using a uniform abstraction regardless of whether the underlying bus has hardware TX echo enabled (arc42 §6.3, ADR-7; FR-RAW-030). When the bus both declares Echo and has WorkMode == ChannelWorkMode.Echo configured, confirmation comes from an actually-matched echo frame (FR-RAW-031: byte-identical sends pending at the same time are matched FIFO, one echo each, never to each other's — no cross-matching or crash). The matching is by content, so an echo of a send that has already timed out or been cancelled cannot be told from the next identical send's and is matched to that send (#349); otherwise it is a documented approximation based on driver acceptance (IsApproximated, FR-RAW-032) -- that is, on the bus's transmit returning a positive count, which means what that bus makes it mean: on a bus wrapped in CanKit's QueuedCanBus it means queued in process, not taken by a driver. Never hangs: timeout, bus-off, and outright rejection all resolve the returned task within bounded time (FR-RAW-033) — see TxConfirmation for exactly how.

Which adapters can take the echo path. It needs the adapter to hand the sent frame back flagged (IsEcho). Measured against CanKit 0.5.6: PCAN in Echo mode delivers its own frames unflagged and Kvaser delivers none on a quiet bus, so on both every confirmation times out although the frame was on the wire; Vector is not measured and is read from its source as not delivering one in Echo mode either. Use Normal with those adapters: the approximated confirmation is what they support. See docs/reviews/2026-10-06-echo-hardware-measurements.md.

System.Threading.Tasks.Task<CanKit.Pro.RawCan.TxConfirmation> SendConfirmedAsync(CanKit.Abstractions.API.Can.Definitions.CanFrame frame, System.Nullable<System.TimeSpan> timeout=null, System.Threading.CancellationToken cancellationToken=default(System.Threading.CancellationToken));

Parameters

frame CanFrame

The frame to send. As with ICanBus.Transmit(CanFrame@), the caller remains the owner (TX-lease) and is responsible for disposing it after this call returns/completes — ICanBusService never disposes it. A CAN-FD payload whose length is not one a CAN-FD frame can carry (0–8, 12, 16, 20, 24, 32, 48 or 64 bytes) is not refused here. Its DLC rounds up to the next of those lengths, so on a real bus it goes out padded to that length. On the echo path the confirmation is matched by content, so on an adapter that echoes the frame as it went on the wire -- padded -- the echo is never matched and the call ends as Timeout although the frame was sent; CanKit 0.5.6's Vector CAN-FD transceiver builds a received frame's length from its DLC that way. Pass a canonical length to avoid it. Refusing a non-canonical length is a behaviour change, kept for the next major version (#352, #299).

timeout Nullable<TimeSpan>

Maximum time to wait before failing with Timeout (FR-RAW-034); null uses DefaultConfirmTimeout. On the echo path it bounds the wait for the echo; on the approximated path it bounds the wait for the driver to accept the frame (ICanBus.TransmitAsync(CanFrame, CancellationToken)), which every current adapter completes immediately, so it only bites for a bus whose asynchronous transmit stalls. A frame reported as timed out may still reach the wire later (the bus is asked to cancel the transmit, but a driver that ignores that cannot be stopped). Must be positive; a non-positive timeout throws ArgumentOutOfRangeException before anything is handed to the driver. A timeout above Int32.MaxValue milliseconds (about 24.8 days, the longest delay the runtime's timer measures on every target) is clamped to that bound, so TimeSpan.MaxValue waits about 24.8 days.

cancellationToken CancellationToken

Caller-supplied cancellation; cancels the returned task per standard .NET convention, distinct from the domain-level Timeout outcome. It is consulted immediately before the driver call — on the echo path inside the send lock, after any wait for another send's driver call — so a token cancelled up to that point abandons the send with OperationCanceledException and nothing reaches the driver (#294). A frame the driver has been handed goes out; the CanKit 0.5.6 adapters ignore the token inside their transmit. Disposing the service cancels an outstanding call the same way, on either path -- while it waits for the echo, or while an asynchronous ICanBus.TransmitAsync(CanFrame, CancellationToken) is still pending -- and it ends in OperationCanceledException (#352).

Returns

Task<TxConfirmation>

Exceptions

ObjectDisposedException
The service is disposed, or is disposed while the call is still on its way to the driver. On either path the disposed check and the hand-off to the driver are one step with respect to IDisposable.Dispose: once Dispose has returned, no send's frame is handed to the driver any more, and a send that loses that race fails with this exception -- on the approximated path since #430, as the echo path always did. A send that won it has handed its frame over -- Dispose does not return while such a send is still inside the driver call -- and if that transmit is still pending, Dispose cancels the call as described under cancellationToken. Sends do not wait for each other's driver call on the approximated path; only Dispose does. The exception is a Dispose called on a thread that is inside a driver call of any CanBusService -- this one or another, on either path (a driver disposing on a fault): it waits for no send, since sends queued behind that call -- behind a lock the driver shares between services, say -- may depend on it. In that case the guarantee above does not hold: a send already past the check may still reach the driver after Dispose has returned.

ICanBusService.Subscribe(CanIdFilter, Nullable<int>, bool) Method

Registers a subscription using the allocation-free ID-range/mask fast path (FR-RAW-010/013).

CanKit.Pro.RawCan.ISubscription Subscribe(CanKit.Pro.RawCan.CanIdFilter filter, System.Nullable<int> bufferCapacity=null, bool includeEcho=false);

Parameters

filter CanIdFilter

ID-range or acceptance-code/mask filter. A default-initialized struct (default(CanIdFilter), or a field never assigned) is accepted and matches the standard ID 0 only -- see CanIdFilter; the predicate overload with a null predicate is the one that accepts every frame.

bufferCapacity Nullable<Int32>

Bounded buffer capacity for this subscription; null uses DefaultBufferCapacity.

includeEcho Boolean

Whether this subscription also receives the local host's own transmit echoes; false by default. See the predicate overload above.

Returns

ISubscription

ICanBusService.Subscribe(Func<CanFrameEvent,bool>, Nullable<int>, bool) Method

Registers a subscription that receives every frame for which predicate returns true; a null predicate accepts all frames (FR-RAW-010).

CanKit.Pro.RawCan.ISubscription Subscribe(System.Func<CanKit.Pro.RawCan.CanFrameEvent,bool>? predicate=null, System.Nullable<int> bufferCapacity=null, bool includeEcho=false);

Parameters

predicate Func<CanFrameEvent,Boolean>

Per-frame filter, or null to accept all frames. Runs on the bus's dispatch thread before the payload is copied, so the Frame it inspects aliases the adapter's RX lease and must not be retained beyond the call. It is never offered an echo unless includeEcho is set.

bufferCapacity Nullable<Int32>

Bounded buffer capacity for this subscription; null uses DefaultBufferCapacity. When the buffer is full the oldest buffered frame is dropped so dispatch never blocks (FR-RAW-011).

includeEcho Boolean

Whether this subscription also receives the local host's own transmit echoes. Defaults to false: a protocol layer that sees its own transmissions come back as if they were peer traffic misbehaves in ways that are tedious to diagnose, so opting in is a decision the caller makes deliberately (see the remarks below).

Returns

ISubscription

Events

ICanBusService.BackgroundExceptionOccurred Event

Raised when a caller-supplied subscription filter predicate throws during dispatch (FR-RAW-023-style fault channel). The failing frame is isolated to that subscription (delivery to the other subscriptions continues), and the exception is surfaced here instead of being silently swallowed. For a predicate fault it is invoked synchronously on the bus's dispatch thread, so handlers must return quickly and must not call back into the service.

Also raised for a failure of the callback, or of the delivery pump, of a Subscribe(this ICanBusService, Action<CanFrameEvent>, Func<CanFrameEvent,bool>, Nullable<int>, bool, Action<Exception>) subscription given no onError. That one is invoked on the subscription's delivery task, a thread-pool thread, not on the dispatch thread.

Also raised, with the adapter's own exception, whenever the bus reports a fault through ICanBus.FaultOccurred. Under the default CanExceptionPolicy (a lower FaultThreshold or a custom Classifier can make any reported error one) the only fault a CanKit 0.5.6 adapter raises is its receive loop dying, after which no further frame arrives on this bus: outstanding SendConfirmedAsync(CanFrame, Nullable<TimeSpan>, CancellationToken) calls end as Timeout and Frames streams stay open until disposed. A consumer that only reads subscriptions learns of it here, not from its stream: an ISubscription carries no fault signal of its own, so such a consumer keeps the service it subscribed on and handles this event (#348). Invoked synchronously on whichever thread the adapter raises its fault on.

event EventHandler<Exception>? BackgroundExceptionOccurred;

Event Type

EventHandler<Exception>