Skip to content

Latest commit

 

History

History

README.md

CanKit.Pro.J1939

Application-layer SAE J1939 node for CanKit.Pro, covering SRS FR-J1939-001..006 (Must) and FR-J1939-007 (Should).

What it does

  • PGN send/receive with 29-bit Priority / PF / PS / SA encoding & decoding via CanKit.Pro.Addressing.J1939Id (FR-J1939-001).
  • SPN extraction from PGN payloads with configurable resolution and offset (little-endian, 1..64-bit fields, unsigned or two's-complement signed) via J1939Spn (FR-J1939-002). Extraction returns a J1939SpnValue, not a double: 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, and a 16-bit engine-speed field reading 0xFFFF means the ECU does not have the parameter — not 8191.875 rpm.
  • Address claiming (PGN 0xEE00) with SAE J1939-81 §4.4.3 NAME arbitration and the 250 ms announcement window (FR-J1939-003).
  • Address-Claim fallback (FR-J1939-004): after losing the preferred address to a higher-priority NAME — during the arbitration or after the claim had succeeded — the node scans the arbitrary address field (0x80..0xF7, wrapping once) and only broadcasts Cannot Claim (PGN 0xEE00 from SA = 0xFE) when the field is exhausted. An equal NAME on that address is not a win for either CA (SAE J1939-81 §4.4.3.3) and ends in Cannot Claim without that scan. Governed by J1939NodeOptions.EnableArbitraryAddressClaiming (default: derived from the NAME's Arbitrary Address Capable bit). A move after a successful claim is announced through AddressClaimChanged; nobody awaits it. The Cannot Claim, and the next claim after losing, go out after the pseudo-random 0..153 ms backoff of SAE J1939-81 §4.4.4.3 (the low byte of the NAME's bytes summed, times 0.6 ms), so two nodes colliding on an address do not answer in lockstep; and a second ClaimAddressAsync while one is in arbitration faults with InvalidOperationException rather than silently cancelling the first (#58).
  • Request for Address Claimed (SAE J1939-81 §4.2.2): a Request for PGN 0xEE00 is answered by the node itself — with its Address Claimed while it holds or arbitrates an address, with Cannot Claim while it holds none — so a network-management tool scanning the bus sees it. A Cannot Claim answer waits the §4.4.4.3 backoff below and shares it with the one a lost claim owes, since every Cannot Claim carries the same null source address; a claim still waiting its backoff answers by starting its round. The request still reaches MessageReceived. ClaimAddressAsync faults only once the Cannot Claim it owes has gone out, so a caller that disposes the node on the exception cannot suppress it.
  • Request-PGN (PGN 0xEA00) send and receive (FR-J1939-005).
  • Auto-routing to J1939-TP for payloads > 8 bytes; direct 29-bit frames for payloads ≤ 8 bytes (FR-J1939-006).
  • Periodic PGN send (FR-J1939-007): every periodic PGN — single- frame and multi-frame alike — is emitted on a fixed-rate grid anchored on the L2 DeadlineScheduler (t0 + n × period), so the long-run rate does not drift by the per-emission send time; ticks whose previous emission is still in flight are skipped and ticks that fell behind are coalesced. The schedule snapshots the caller's payload into an owned buffer at Start-time and hands SendAsync the same immutable bytes on every tick, so in-place edits to the caller buffer after StartPeriodicSend are not observable on the wire. SendAsync's pre-flight claim gate runs on every emission, so the schedule stops putting frames on the wire as soon as the node leaves Claimed and resumes automatically after a fresh claim — the emitted 29-bit ID is composed from the currently-claimed SA. Send failures (including J1939NoAddressException from the claim gate) are surfaced via BackgroundExceptionOccurred. The caller supplies the transmit period; mapping application PGNs to their SAE J1939-71 standard rate is the caller's responsibility (no PGN rate catalog is embedded). A native L1 IPeriodicTx optimization for single-frame PGNs remains a follow-up — its L1 error-propagation blocker is resolved (IPeriodicTx.Faulted).

Architecture

  • Composes strictly on L2 (CanKit.Pro.RawCan.ICanBusService, CanKit.Pro.Actor.IProtocolActor, CanKit.Pro.Reliability.DeadlineScheduler) — no vendor SDK dependency.
  • Uses CanKit.Pro.Addressing helpers (J1939Id, J1939Pgn, J1939Fields, J1939Name); the node never reimplements ID / PGN / NAME math.
  • Delegates multi-frame transport to CanKit.Pro.J1939Tp per FR-J1939-006.
  • Follows the same factory / interface / impl pattern as CanKit.Pro.Uds and CanKit.Pro.J1939Tp.

Usage

var options = new J1939NodeOptions(
    new J1939Name(
        identityNumber: 0x12345,
        manufacturerCode: 0x0AB,
        ecuInstance: 0, functionInstance: 0, function: 0x81,
        reserved: false,
        vehicleSystem: 0, vehicleSystemInstance: 0,
        industryGroup: 0, arbitraryAddressCapable: false));

using var bus = CanBus.Open("virtual://demo/0", cfg => cfg.SetProtocolMode(CanProtocolMode.Can20));
using var node = J1939Node.Open(bus, options);

await node.ClaimAddressAsync(preferredAddress: 0x11);

// Direct single-frame PGN (≤ 8 bytes).
await node.SendAsync(new J1939Message(pgn: 0xFEF0, payload: new byte[] { 1, 2, 3, 4 }));

// Multi-frame PGN (> 8 bytes) auto-routes through J1939-TP.
await node.SendAsync(new J1939Message(pgn: 0xFECA, payload: new byte[64]));

// Request-PGN.
await node.RequestPgnAsync(requestedPgn: 0xFEF1, destinationAddress: 0xFF);

// SPN extraction (little-endian, physical = raw * resolution + offset).
node.MessageReceived += (_, msg) =>
{
    J1939SpnValue speed = J1939Spn.Extract(msg.Payload.Span,
        byteOffset: 3, startBit: 0, bitLength: 16,
        resolution: 0.125, offset: 0.0);

    // .Value throws unless the field really carries a measurement.
    if (speed.TryGetValue(out double rpm)) Use(rpm);
    else if (speed.IsNotAvailable) { /* the ECU does not have this parameter */ }
    else if (speed.IsError)        { /* the ECU flagged its own reading as wrong */ }

    // Or, for arithmetic that should stay visibly wrong rather than plausible:
    double rpmOrNaN = speed.GetValueOrDefault();          // NaN unless valid

    // Signed SPNs (two's-complement SLOTs) pass isSigned: true.
    J1939SpnValue trim = J1939Spn.Extract(msg.Payload.Span,
        byteOffset: 0, startBit: 0, bitLength: 16,
        resolution: 0.1, offset: 0.0, isSigned: true);
};

The indicator ranges follow SAE J1939-71 §5.1.1 and scale with the field width — for one byte 0xFB / 0xFC..0xFD / 0xFE / 0xFF, for two bytes the same codes in the leading byte (0xFF00..0xFFFF is not available), and with the sign bit cleared (0x7B..0x7F) for a signed SPN. Raw on the returned value is always the bit pattern as read off the wire, so the exact code can still be logged or forwarded. J1939Spn.Classify exposes the range check on its own, and J1939SpnDefinition carries an IsSigned flag so the catalog decodes signed parameters too.

Status

1.3.0 is the first stable release: from 1.3.0 on the public API follows SemVer, so a breaking change costs a major version. 1.0.0 – 1.2.3 were published as stable before the API had been reviewed against the specifications; they are unlisted and deprecated on nuget.org and should not be used. See Versioning.

What is validated, and what is not

Validated: PGN send and receive, SPN extraction and its indicator values, address claiming and its fallback, Request-PGN and periodic sends, by the test suite in tests/CanKit.Pro.Tests, between nodes of this implementation over CanKit.Adapter.Virtual, directly or through a controllable bus double.

Not validated: Address-claim arbitration against third-party ECUs and behaviour on a real J1939 network. Nothing in this package has run against real CAN hardware, a conformance tester or a third-party implementation: the test project references CanKit.Adapter.Virtual and no hardware adapter.

Install

dotnet add package CanKit.Pro.J1939

# plus a CanKit adapter for the hardware you actually talk to, e.g.
dotnet add package CanKit.Adapter.Virtual   # loopback, no hardware

Dependencies: CanKit.Abstractions, CanKit.Pro.Actor, CanKit.Pro.Addressing, CanKit.Pro.J1939Tp, CanKit.Pro.RawCan, CanKit.Pro.Reliability.

Part of CanKit.Pro — higher CAN protocol layers built on top of CanKit, which is consumed as a NuGet package rather than forked.

License

MIT — see LICENSE. CanKit itself is a separate project licensed under Apache-2.0; see THIRD-PARTY-NOTICES.md.