@csllc/j1939
v2.0.0
Published
J1939 transport layer for CANBUS communication
Readme
@csllc/j1939
A TypeScript implementation of the SAE J1939 CANBUS protocol for Node.js. Provides J1939 transport layer functionality — including address claiming (J1939-81) and multi-packet transport protocol (J1939-21) — on top of a compatible CANBUS interface driver.
This package is intended primarily for testing and diagnostic purposes. The implementation is not fully J1939 compliant, does not necessarily implement strict packet timings, and may have missing or broken features.
Installation
npm install @csllc/j1939Module Formats
The package ships with dual build outputs:
- ESM (
import):dist/esm/index.js - CommonJS (
require):dist/cjs/index.js - TypeScript declarations:
dist/esm/index.d.ts/dist/cjs/index.d.ts
Node.js automatically resolves the correct format via the exports field in package.json.
Exports
Classes
J1939
The main protocol handler class. Extends EventEmitter.
import { J1939 } from '@csllc/j1939';
// or
const { J1939 } = require('@csllc/j1939');Constructor: new J1939(bus: CanBus, options?: J1939Options)
| Parameter | Description |
|-----------|-------------|
| bus | A CANBUS driver instance implementing the CanBus interface (Node.js Duplex stream with write(), isOpen(), close(), _flushRequestQueue()) |
| options | Optional configuration (see J1939Options below) |
Methods:
| Method | Signature | Description |
|--------|-----------|-------------|
| write | (msg: J1939WriteMessage) => void | Send a J1939 PGN. Messages <= 8 bytes are sent as single CAN frames. Larger messages automatically use Transport Protocol (BAM for broadcast, RTS/CTS for addressed). |
| setOptions | (options: J1939Options) => void | Update configuration options and reset address claim state. |
| setAddress | (name?, preferredAddress?, addressRange?) => Promise<void> | Run the J1939-81 address claim procedure. Called automatically on bus open. |
| isReady | () => boolean | Returns true if address claim is complete and bus is open. |
| isOpen | () => boolean | Returns true if the underlying bus connection is open. |
| close | () => void | Close the bus and clean up all pending transactions. |
| buildId | (pgn: number, to: number, pri?: number) => number | Build a 29-bit CAN identifier from a PGN, destination, and priority. |
| compareName | (a: number[], b: number[]) => number | Compare two 8-byte J1939 NAMEs for arbitration priority. Returns -1 if a wins, 1 if b wins. |
Events:
| Event | Payload | Description |
|-------|---------|-------------|
| open | (address: number) | Address claim succeeded; protocol is ready. |
| data | (msg: J1939Message) | Incoming J1939 PGN addressed to this node or broadcast. |
| rx | (msg: CanMessage) | Incoming non-J1939 message (11-bit standard CAN frame). |
| address | (status: AddressClaimStatus, attemptedAddress: number) | Address claim status update. |
| close | () | Bus and protocol shut down. |
| error | (err: Error) | Error from J1939 processing or the CANBUS interface. |
Enums
AddressClaimStatus
import { AddressClaimStatus } from '@csllc/j1939';
AddressClaimStatus.NONE // 0 - No claim attempted
AddressClaimStatus.IN_PROGRESS // 1 - Claim in progress
AddressClaimStatus.SUCCESSFUL // 2 - Address claimed successfully
AddressClaimStatus.FAILED // 3 - Claim failed
AddressClaimStatus.LISTEN_ONLY // 4 - Listen only (not implemented)Interfaces
J1939Options
Configuration options for the J1939 constructor.
interface J1939Options {
address?: number; // Preferred source address (0x00-0xFD), default 0xFE
preferredAddress?: number; // Alias for address
name?: number[]; // 8-byte J1939 NAME for address arbitration
addressRange?: [number, number]; // [min, max] fallback address range
bamInterval?: number; // BAM packet interval in ms (default 50)
priority?: number; // Default message priority 0-7 (default 4)
sendClaimRequest?: boolean; // Send Request for Address Claimed during claim
}J1939WriteMessage
Message format for write().
interface J1939WriteMessage {
pgn: number; // Parameter Group Number
dst: number; // Destination address (0xFF for broadcast)
buf: Buffer; // Payload data
src?: number; // Source address override
priority?: number; // Priority 0-7 (0 = highest)
cb?: (err: Error | null) => void; // Completion callback for TP transfers
}J1939Message
Received message format emitted by the data event.
interface J1939Message {
pri: number; // Priority 0-7
src: number; // Source address
dst: number; // Destination address
pgn: number; // Parameter Group Number
buf: Buffer; // Payload data
}CanMessage
Raw CAN frame as seen on the bus.
interface CanMessage {
id: number; // 11-bit or 29-bit CAN identifier
ext?: boolean; // true for 29-bit extended frame
buf: Buffer | number[]; // Frame payload
}CanBus
Interface that a CANBUS driver must implement. Must be a Node.js Duplex-compatible stream that emits open, data, error, and close events.
interface CanBus extends EventEmitter {
write(msg: CanMessage): any;
isOpen(): boolean;
close(): void;
_flushRequestQueue(): void;
}Quick Start
CommonJS
const Canbus = require('can-usb-com');
const { J1939 } = require('@csllc/j1939');
const bus = new Canbus({ canRate: 250000 });
const j1939 = new J1939(bus, {
address: 0x80,
name: [0, 0, 0, 0, 0, 0, 0, 0],
});
j1939.on('open', (address) => {
console.log('J1939 ready with address', address);
j1939.on('data', (msg) => {
console.log('Received PGN', msg.pgn.toString(16), 'from', msg.src);
});
// Send a short message (single CAN frame)
j1939.write({
pgn: 0xEF00,
dst: 100,
priority: 7,
buf: Buffer.from([0x01, 0x02, 0x03]),
});
});ESM / TypeScript
import { J1939, AddressClaimStatus } from '@csllc/j1939';
import type { J1939Options, J1939Message, CanBus } from '@csllc/j1939';
const bus: CanBus = getCanBusInterface(); // your CANBUS driver
const options: J1939Options = {
address: 0x80,
name: [1, 2, 3, 4, 5, 6, 7, 8],
addressRange: [0x80, 0xEF],
priority: 6,
};
const j1939 = new J1939(bus, options);
j1939.on('address', (status, address) => {
if (status === AddressClaimStatus.SUCCESSFUL) {
console.log('Claimed address', address);
}
});
j1939.on('data', (msg: J1939Message) => {
console.log(`PGN 0x${msg.pgn.toString(16)} from ${msg.src}:`, msg.buf);
});Multi-Packet Transfer
Messages larger than 8 bytes are automatically sent using the J1939 Transport Protocol:
const { J1939 } = require('@csllc/j1939');
const j1939 = new J1939(bus, { address: 0x80 });
j1939.on('open', () => {
// Send a large message via TP (BAM for broadcast)
j1939.write({
pgn: 0xEF00,
dst: 0xFF, // broadcast
priority: 7,
buf: Buffer.alloc(200).fill(0x55),
});
// Send via RTS/CTS to a specific address, with completion callback
j1939.write({
pgn: 0xEF00,
dst: 0x10,
priority: 7,
buf: Buffer.alloc(200).fill(0xAA),
cb: (err) => {
if (err) {
console.error('Transfer failed:', err.message);
} else {
console.log('Transfer complete');
}
},
});
});Address Claiming with Fallback Range
const { J1939 } = require('@csllc/j1939');
const j1939 = new J1939(bus, {
address: 0x80, // preferred address
name: [1, 2, 3, 4, 5, 6, 7, 8], // 8-byte NAME for arbitration
addressRange: [0x80, 0xEF], // fallback range if preferred is taken
priority: 6,
});
j1939.on('address', (status, address) => {
console.log('Address claim status:', status, 'address:', address);
});
j1939.on('open', (address) => {
console.log('Ready at address', address);
});
j1939.on('error', (err) => {
console.error('Error:', err.message);
});Examples
See the example/ directory for complete working examples:
network_management.js— Demonstrates J1939-81 address claiming with a@csllc/cs-canbus-universaladapterpingpong.js— Bidirectional multi-packet messaging between two CAN-USB-COM devices
Development
Building
npm run build # Build both ESM and CJS
npm run build:esm # Build ESM only
npm run build:cjs # Build CJS only
npm run clean # Remove dist/Testing
npm test # Build + run all tests
npx mocha test/offline/claim.test.js # Run a specific test fileOffline tests in test/offline/ use a mock bus and do not require CAN hardware.
Linting
npm run lintESLint rules are stored in .eslintrc.js.
License
MIT
