@csllc/j1939
Version:
J1939 transport layer for CANBUS communication
1,137 lines (881 loc) • 33.5 kB
JavaScript
/**
* Defines a NodeJS class that implements J1939 CANBUS message processing
*
* Requires a compatible CANBUS driver
*
*/
const EventEmitter = require('events').EventEmitter;
const DEFAULT_OPTIONS = {
address: 0xFE,
name: [0, 0, 0, 0, 0, 0, 0, 0],
bamInterval: 50,
priority: 4,
};
/* SAE J1939 Source Addresses found in J1939 p. 45. 252 is
for experimental use */
const J1939_ADDR_DIAG_TOOL1 = 249; // 0xF9
const J1939_ADDR_EXPERIMENTAL_USE = 252; // 0xFC
const J1939_ADDR_NULL = 254; // 0xFE
const J1939_ADDR_GLOBAL = 255; // 0xFF
/* SAE J1939 parameter group numbers */
const J1939_PGN_REQUEST = 59904; // 0xEA00
const J1939_PGN_ADDRESS_CLAIMED = 60928; // 0xEE00
const J1939_PGN_CANNOT_CLAIM_ADDRESS = J1939_PGN_ADDRESS_CLAIMED;
const J1939_PGN_PROPRIETARY_A = 61184; // 0xEF00
const J1939_PGN_TP_CM = 60416; // 0xEC00
const J1939_PGN_TP_DT = 60160; // 0xEB00
/* different transport protocol states */
const J1939TP_STATE_UNUSED = 0;
const J1939TP_STATE_SEND_BAM = 1;
const J1939TP_STATE_SEND_RTS = 2;
const J1939TP_STATE_SEND_CTS = 3;
const J1939TP_STATE_SEND_DATA = 4;
const J1939TP_STATE_SEND_ACK = 5;
const J1939TP_STATE_SEND_ABORT = 6;
const J1939TP_STATE_WAIT_CTS = 7;
const J1939TP_STATE_WAIT_DATA = 8;
const J1939TP_STATE_WAIT_ACK = 9;
/* type of TP msg (see spec -21) */
const J1939TP_CTRL_BYTE_RTS = 16;
const J1939TP_CTRL_BYTE_CTS = 17;
const J1939TP_CTRL_BYTE_ACK = 19;
const J1939TP_CTRL_BYTE_BAM = 32;
const J1939TP_CTRL_BYTE_ABORT = 255;
/* reasons to send an abort msg (see spec -21) */
const J1939TP_RSN_BUSY = 1;
const J1939TP_RSN_RSRCS_FREED = 2;
const J1939TP_RSN_TIMEOUT = 3;
const J1939TP_RSN_ERROR = 254;
// timeouts in ms (see spec -21)
const J1939TP_TIMEOUT_BM = (50);
const J1939TP_TIMEOUT_TR = (200);
const J1939TP_TIMEOUT_TH = (500);
const J1939TP_TIMEOUT_T1 = (750);
const J1939TP_TIMEOUT_T2 = (1250);
const J1939TP_TIMEOUT_T3 = (1250);
const J1939TP_TIMEOUT_T4 = (1050);
// max size of incoming message
const J1939CFG_TP_RX_BUF_SIZE = 1792;
/**
* Convert an abort code to a string
*
* @param {<type>} code The code
* @return {string} Text description of abort code
*/
function abortReason(code) {
switch (code) {
case 1:
return 'Busy';
case 2:
return 'RSRCS Freed';
case 3:
return 'Timeout';
case 254:
return 'Error';
default:
return 'Unknown';
}
}
/**
* Class that holds j1939 transaction state and data
*
* @class J1939Transaction (name)
*/
class J1939Transaction {
constructor(msg, src, type) {
this.type = type;
if(type === 'tx') {
this.dst = msg.dst;
this.src = src;
this.pgn = msg.pgn;
this.msgSize = msg.buf.length;
this.numPackets = Math.floor((msg.buf.length + 6) / 7);
this.numCur = 1;
this.ctsCnt = 0;
this.ctsRcvd = 0;
this.buf = msg.buf;
this.cb = msg.cb;
this.timer = null;
this.state = null;
} else {
this.state = null;
this.pgn = msg.buf[5] * 65536 + msg.buf[6] * 256 + msg.buf[7];
this.msgSize = msg.buf[1] | msg.buf[2] << 8;
this.nextSeq = 1;
this.numPackets = msg.buf[3];
this.ctsMax = msg.buf[4];
this.ctsCnt = 0;
this.dst = src;
this.src = msg.src;
this.pri = msg.pri;
this.buf = Buffer.alloc(this.msgSize);
}
}
}
/**
* Class definition we are exporting
*/
module.exports = class J1939 extends EventEmitter {
constructor(bus, options) {
super();
this.requestQueue = [];
this.setOptions(options);
this.bus = bus;
// If bus is already open, start the address claim process
// Otherwise we will do it when the bus emits the open event
if(bus.isOpen()) {
this.setAddress()
.catch((e) => {
console.error(e);
});
}
// Bind bus events to our handlers
bus.on('open', this.onBusOpen.bind(this));
bus.on('data', this.onBusData.bind(this));
bus.on('error', this.onBusError.bind(this));
bus.on('close', this.onBusClose.bind(this));
}
// sets (or re-sets) the configuration options.
setOptions(options) {
// save for later use
this.options = Object.assign(DEFAULT_OPTIONS, options);
// When claiming address, send a request for claimed addresses
// instead of simply sending an 'address claimed' message
this.sendClaimRequest = true || this.options.sendClaimRequest;
// address claim states
this.CLAIM_NONE = 0;
this.CLAIM_IN_PROGRESS = 1;
this.CLAIM_SUCCESSFUL = 2;
this.CLAIM_FAILED = 3;
this.CLAIM_LISTEN_ONLY = 4; // not implemented
// one or the other of these needs to be defined
this.preferredAddress = this.options.preferredAddress || this.options.address;
if (this.options.addressRange) {
if (Array.isArray(this.options.addressRange) && (this.options.addressRange.length == 2)) {
this.addressRange = this.options.addressRange;
} else {
throw new Error("Address range must be array: [ min, max ]");
}
}
this.address = 0; // until claim procedure is done
this.name = this.options.name;
this.priority = this.options.priority;
this.addressClaimStatus = this.CLAIM_NONE;
this.attemptedSourceAddress = 0;
this.unavailableAddresses = [];
// default interval to 50ms (J1939 standard)
this.bamInterval = this.options.bamInterval;
// a list of the transport protocol transactions we have in process
this.transactions = [];
}
// returns true if we are ready for business
isReady() {
return this.bus && this.bus.isOpen() && (this.addressClaimStatus === this.CLAIM_SUCCESSFUL);
}
// returns true if the bus is open (i.e., the underlying connection is ready)
isOpen() {
return this.bus.isOpen();
}
// Close the serial port and clean up
close() {
this.bus._flushRequestQueue();
if(this.isOpen()) {
this.bus.close();
}
this.bus = null;
this.isReady = false;
}
reportAddressClaimStatus() {
this.emit('address', this.addressClaimStatus, this.attemptedSourceAddress);
}
// compare two 8-byte J1939 NAMEs
compareName(a, b) {
// Find first different byte
let i;
for (i = 7; ((a[i] == b[i]) && (i > 0)); i--) { ; }
if (a[i] <= b[i]) {
return -1;
} else if (a[i] > b[i]) {
return 1;
} else {
return 0;
}
}
// Attempt to claim a single address
claimAddress(name, address, isPreferred = false) {
let me = this;
return new Promise((resolve, reject) => {
if (!address || me.unavailableAddresses.includes(address)) {
reject();
return;
}
me.addressClaimStatus = me.CLAIM_IN_PROGRESS;
me.attemptedSourceAddress = address;
me.reportAddressClaimStatus();
let timer;
// Listener for Address Claimed messages from other devices
let listener = function(msg) {
// console.log("listener msg", msg);
let pf = (msg.id >> 16) & 0xFF; // protocol format field
let ps = (msg.id >> 8) & 0xFF; // protocol specific field
let sa = (msg.id) & 0xFF; // source address
if(pf == 0xEE && ps == 0xFF && sa == me.attemptedSourceAddress) {
// Check who wins arbitration based on J1939 NAME
if (me.compareName(me.name, [...msg.buf]) < 0) {
// If our name is higher priority, the address is ours.
// Ignore their Address Claimed message; they must relinquish it
// to us per J1939-81 2017-03 4.5.3.3
} else {
// Otherwise the address is in use by a device with a higher
// priority NAME, so we can't take it. Note it and let the caller
// decide what to do.
clearTimeout(timer);
me.unavailableAddresses.push(address);
reject();
}
}
};
me.on('rx', listener);
if (isPreferred && me.sendClaimRequest) {
// send Request for Address Claimed
me.bus.write({ id: (me.priority << 26) + (J1939_PGN_REQUEST << 8) + (address << 8) + 0xFE, ext: true, buf: [ 0x00, 0xEE, 0x00 ] });
} else {
// send Address Claimed
me.bus.write({ id: (me.priority << 26) + (J1939_PGN_ADDRESS_CLAIMED << 8) + (J1939_ADDR_GLOBAL << 8) + (address), ext: true, buf: name });
me.sentAddressClaimed = true;
}
// wait at least 250ms, if no response then we can use address.
// If there is a response, the listener function above will kill this
// timer before it expires.
timer = setTimeout(function() {
// we hit the timeout, address is ours!
me.removeListener('rx', listener);
resolve();
}, 500);
});
}
// returns a promise that resolves when the address claim process is complete
setAddress(name, preferredAddress, addressRange) {
let me = this;
// unless specified, use stored values
me.name = name || me.name;
me.preferredAddress = preferredAddress || me.preferredAddress;
me.addressRange = addressRange || me.addressRange;
// First try our preferred address
return me.claimAddress(me.name, me.preferredAddress, true)
.catch(() => {
// If we fail to get our preferred address, keep trying per J1939-81
// 2017-03, section 4.5.3.4. Iterate through our address range if one was
// provided, minus addresses we already know aren't available.
return new Promise(async (resolve, reject) => {
if (!me.addressRange) {
reject();
}
if (me.addressRange) {
for (let addr = me.addressRange[0]; addr <= me.addressRange[1]; addr++) {
try {
await me.claimAddress(me.name, addr);
resolve();
break;
} catch(e) {
// if rejected, try next address by falling through to next
// loop iteration
;
}
}
// If none of the claimAddress calls above succeed, then
// reject the whole thing
reject();
} else {
reject();
}
})
})
.then(() => {
me.address = me.attemptedSourceAddress;
if (!me.sentAddressClaimed) {
// only send if not already done
me.bus.write({ id: (me.priority << 26) + (J1939_PGN_ADDRESS_CLAIMED << 8) + (J1939_ADDR_GLOBAL << 8) + me.address, ext: true, buf: me.name });
}
me.addressClaimStatus = me.CLAIM_SUCCESSFUL;
me.reportAddressClaimStatus();
me.emit('open', me.address);
})
.catch(() => {
let delay = Math.floor(Math.random() * 153); // Random delay is 0 - 153ms
setTimeout(function() {
return me.bus.write({ id: (me.priority << 26) + (J1939_PGN_CANNOT_CLAIM_ADDRESS << 8) + (J1939_ADDR_GLOBAL << 8) + J1939_ADDR_NULL, ext: true, buf: me.name })
.then(() => {
me.addressClaimStatus = me.CLAIM_FAILED;
me.reportAddressClaimStatus();
me.emit('error', new Error('CAN Address already in use'));
})
}, delay);
});
}
// Send a J1939 PGN. If >8 bytes, it is sent using transport protocol
/**
* Send a J1939 PGN
*
* @param {Object} msg contains the message to be sent
* @param {number} msg.pgn the PGN of the message
* @param {Buffer} msg.buf the data payload of the message
* @param {number} msg.dst the address where the message should be sent
* @param {number} msg.src the source address, optional (default to previously determined address)
* @param {number} msg.priority 0-7, optional, 0 being highest priority
* @param {function} msg.cb, optional function(err) to call on completion
*/
write(msg) {
let pri = msg.priority || 0x07;
if(msg.buf.length <= 8) {
this.bus.write({ id: this.buildId(msg.pgn, msg.dst, pri), ext: true, buf: msg.buf });
} else if(this.address) {
// use transport protocol if J1939 enabled
let transaction = new J1939Transaction(msg, this.address, 'tx');
// add to our list of transactions
this.transactions.push(transaction);
if(msg.dst === J1939_ADDR_GLOBAL) {
transaction.state = J1939TP_STATE_SEND_BAM;
this.sendBamOrRts(transaction, J1939TP_CTRL_BYTE_BAM);
transaction.timer = setTimeout(this.updateTxBam.bind(this, transaction), this.bamInterval);
} else {
transaction.state = J1939TP_STATE_SEND_RTS;
this.sendBamOrRts(transaction, J1939TP_CTRL_BYTE_RTS);
transaction.state = J1939TP_STATE_WAIT_CTS;
transaction.timer = setTimeout(this.onTpTimeout.bind(this, transaction), J1939TP_TIMEOUT_T3);
}
} else {
throw new Error('Invalid message data size or no address');
}
}
// handle an incoming 29-bit CAN message when in J1939 mode
handleJ1939(canmsg) {
let me = this;
var dst, pgn;
var pf = (canmsg.id >> 16) & 0xff;
if(pf < 240) {
// destination-specific
dst = (canmsg.id >> 8) & 0xff;
pgn = (canmsg.id >> 8) & 0x1ff00;
} else {
// broadcast
dst = J1939_ADDR_GLOBAL;
pgn = (canmsg.id >> 8) & 0x1ffff;
}
if(dst === me.address || dst === J1939_ADDR_GLOBAL) {
var src = canmsg.id & 0xFF;
let msg = {
pri: (canmsg.id >> 26) & 0x07,
src: src,
dst: dst,
pgn: pgn,
buf: canmsg.buf,
};
switch (pgn) {
case J1939_PGN_REQUEST:
let request_pgn = msg.buf.readUInt16LE(0);
switch (request_pgn) {
case J1939_PGN_ADDRESS_CLAIMED:
me.bus.write({ id: (me.priority << 26) + (J1939_PGN_ADDRESS_CLAIMED << 8) + (J1939_ADDR_GLOBAL << 8) + (me.address), ext: true, buf: me.name });
break;
}
break;
case J1939_PGN_TP_CM:
me.processCm(msg);
break;
case J1939_PGN_TP_DT:
me.processDt(msg);
break;
case J1939_PGN_ADDRESS_CLAIMED:
if ((msg.src == me.address) && (me.addressClaimStatus == me.CLAIM_SUCCESSFUL)) {
// If another device comes along and claims our address, compare
// NAMEs. If theirs is higher priority, reliquinish the address and
// try to find another one.
if (me.compareName(me.name, [...msg.buf]) > 0) {
me.unavailableAddresses = [ me.address ];
me.setAddress();
} else {
me.bus.write({ id: (me.priority << 26) + (J1939_PGN_ADDRESS_CLAIMED << 8) + (J1939_ADDR_GLOBAL << 8) + (me.address), ext: true, buf: me.name });
}
}
// (intentional fall-through)
default:
// let upper level application handle it
me.emit('data', msg);
break;
}
}
}
onBusOpen() {
this.setAddress()
.catch((e) => {
console.error(e);
})
}
// If a bus error, report it
onBusError(err) {
this.emit('error', err);
}
// When the bus closes, clean up and close too
onBusClose() {
while(this.transactions.length > 0) {
if(this.transactions[0].cb) {
this.transactions[0].cb(new Error('Bus Offline'));
}
this.completeTpTransaction(this.transactions[0]);
}
this.emit('close');
}
// Event handler that is triggered when a valid message arrives on the CANBUS
// If our address claim is complete and it is a 29-bit message,
// process it. Otherwise just pass it up to the anybody who listens
onBusData(msg) {
if(msg.id > 0) {
if(this.address && msg.ext) {
this.handleJ1939(msg);
} else {
// emit a standard (non-J1939) message
this.emit('rx', msg);
}
}
}
/**
* Handles an incoming J1939 CM message
*
* @param The message
*/
processCm(msg) {
// /* all cm messages have the pgn in the same location */
var pgn = msg.buf[5] | msg.buf[6] << 8 | msg.buf[7] << 16;
/* msg_size is in RTS, ACK, and BAM so make sure it's only used there */
var msgSize = msg.buf[1] | msg.buf[2] << 8;
switch (msg.buf[0]) {
// start receiving a multi-packet broadcast message
case J1939TP_CTRL_BYTE_BAM:
//console.log('BAM from ', msg.src);
// valid BAM must have a global dst
if(msg.dst === J1939_ADDR_GLOBAL) {
// first check to make sure we're not receiving a BAM from this addr.
// if we are, then discard the old BAM.
this.cleanRxTransaction(J1939_ADDR_GLOBAL, msg.src);
}
break;
// start receiving a multi-packet message addressed to us
case J1939TP_CTRL_BYTE_RTS:
//console.log('RTS from ', msg.src);
// valid RTS must NOT have a global dst
if(msg.dst !== J1939_ADDR_GLOBAL) {
// first check to make sure we're not receiving a rts/cts from
// this addr. if we are, then discard the old msg.
this.cleanRxTransaction(msg.dst, msg.src);
// use transport protocol if J1939 enabled
let transaction = new J1939Transaction(msg, this.address, 'rx');
transaction.state = J1939TP_STATE_WAIT_DATA;
// add to our list of transactions
this.transactions.push(transaction);
/* does it have a correct size? */
if(msgSize <= msg.buf[3] * 7) {
transaction.ctsCnt = 0;
this.sendCts(transaction);
transaction.timer = setTimeout(this.onTpTimeout.bind(this, transaction), J1939TP_TIMEOUT_T3);
} else {
this.sendAbort(transaction.src, transaction);
this.completeTpTransaction(transaction);
}
}
break;
case J1939TP_CTRL_BYTE_CTS:
{
//console.log('CTS from ', msg.src);
// see if we know about the associated transaction
let transaction = this.findTxTransaction(msg.src, msg.dst, pgn, [J1939TP_STATE_WAIT_CTS, J1939TP_STATE_WAIT_ACK]);
if(transaction) {
// console.log(transaction);
/* if we never get a CTS, then an abort shouldn't be sent. if we did
get a CTS, then an abort needs to be sent if a timeout happens */
transaction.ctsRcvd = 1;
/* spec says only 1 CTS can be received at a time */
if(transaction.ctsCnt) {
transaction.rsn = J1939TP_RSN_ERROR;
transaction.state = J1939TP_STATE_SEND_ABORT;
transaction.timer = setTimeout(this.onTpTimeout.bind(this, transaction), J1939TP_TIMEOUT_TR);
} else if((transaction.state === J1939TP_STATE_WAIT_CTS) || (transaction.state === J1939TP_STATE_WAIT_ACK)) {
if(transaction.timer) {
clearTimeout(transaction.timer);
}
//transaction.state = J1939TP_STATE_SEND_DATA;
transaction.ctsCnt = msg.buf[1];
transaction.numCur = msg.buf[2];
// console.log('before send', transaction);
// let packetsToSend = Math.min(transaction.ctsCnt, transaction.numPackets - transaction.numCur);
// send the next data block
for(var i = 0; i < transaction.numPackets; i++) {
this.sendDt(transaction);
transaction.numCur++;
transaction.ctsCnt--;
}
// console.log('after send', transaction);
if(transaction.numCur >= transaction.numPackets) {
transaction.timer = setTimeout(this.onTpTimeout.bind(this, transaction), J1939TP_TIMEOUT_T3);
transaction.state = J1939TP_STATE_WAIT_ACK;
} else if(transaction.ctsCnt === 0) {
transaction.timer = setTimeout(this.onTpTimeout.bind(this, transaction), J1939TP_TIMEOUT_TR);
transaction.state = J1939TP_STATE_WAIT_CTS;
}
// console.log('after state', transaction);
}
}
}
break;
case J1939TP_CTRL_BYTE_ACK:
{
//console.log('ACK from ', msg.src);
// see if we know about the associated transaction
let transaction = this.findTxTransaction(msg.src, msg.dst, pgn, [J1939TP_STATE_WAIT_ACK]);
if(transaction) {
if(transaction.cb) {
transaction.cb((transaction.msgSize === msgSize) ? null : new Error('Incomplete'));
}
this.completeTpTransaction(transaction);
}
break;
}
case J1939TP_CTRL_BYTE_ABORT:
{
//console.log('ABORT from ', msg.src);
//console.log( 'RX_ABORT', abortReason( msg.buf[1] ));
// see if we know about the associated transaction
let transaction = this.findTxTransaction(msg.src, msg.dst, pgn, [J1939TP_STATE_WAIT_ACK, J1939TP_STATE_WAIT_CTS, J1939TP_STATE_WAIT_DATA]);
if(transaction) {
this.completeTpTransaction();
}
// cnt = j1939tp_txbuf_find(msg->dst, msg->src, pgn);
// if( cnt < J1939CFG_TP_TX_BUF_NUM )
// j1939tp_rtscts_failed(cnt);
// cnt = j1939tp_rxbuf_find(msg->dst, msg->src, pgn);
// if( cnt < J1939CFG_TP_RX_BUF_NUM )
// j1939tp_rxbuf[cnt].state = J1939TP_STATE_UNUSED;
break;
}
}
return;
}
rxBam(transaction, msg) {
/* how many bytes have been received and are remaining? */
let rcv = (transaction.nextSeq - 1) * 7;
let rem = transaction.msgSize - rcv;
/* the entire CAN frame might not be data, so we need to know
how many bytes are suppose to be left */
let min = 7;
if(rem < 7)
min = rem;
switch (transaction.state) {
case J1939TP_STATE_WAIT_DATA:
{
if(transaction.nextSeq === msg.buf[0] && (rcv + min) <= J1939CFG_TP_RX_BUF_SIZE && (rcv + rem) === transaction.msgSize) {
if(transaction.timer) {
clearTimeout(transaction.timer);
}
msg.buf.copy(transaction.buf, rcv, 1, 1 + min);
if(transaction.nextSeq++ === transaction.numPackets) {
this.emit('data', {
pri: transaction.pri,
src: transaction.src,
dst: transaction.dst,
pgn: transaction.pgn,
buf: transaction.buf,
});
this.completeTpTransaction(transaction);
}
}
break;
// /* check to make sure that the sequence number and rx count
// are correct and that an overflow won't occur */
// if( ( j1939tp_rxbuf[ index ].next_seq == msg->buf[ 0 ] ) &&
// ( ( b_rcv + min ) <= J1939CFG_TP_RX_BUF_SIZE ) &&
// ( ( b_rcv + b_rem ) == j1939tp_rxbuf[ index ].msg_size ) )
// {
// j1939tp_rxbuf[ index ].timer = 0;
// for( cnt = 0; cnt < min; cnt++ )
// j1939tp_rxbuf[ index ].buf[ b_rcv + cnt ] = msg->buf[ 1 + cnt ];
// /* was that the last packet? */
// if( j1939tp_rxbuf[ index ].next_seq++ == j1939tp_rxbuf[ index ].num_packets )
// {
// n_msg.pgn = j1939tp_rxbuf[ index ].pgn;
// n_msg.buf = j1939tp_rxbuf[ index ].buf;
// n_msg.buf_len = j1939tp_rxbuf[ index ].msg_size;
// n_msg.dst = j1939tp_rxbuf[ index ].dst;
// n_msg.src = j1939tp_rxbuf[ index ].src;
// J1939_AppProcess( &n_msg );
// j1939tp_rxbuf[ index ].state = J1939TP_STATE_UNUSED;
// }
// }
// else
// {
// /* sequence was wrong, so we discard the entire message */
// j1939tp_rxbuf[ index ].state = J1939TP_STATE_UNUSED;
// }
// break;
}
default:
this.completeTpTransaction(transaction);
break;
}
return;
}
rxRtsCts(transaction, msg) {
/* how many bytes have been received and are remaining? */
let rcv = (transaction.nextSeq - 1) * 7;
let rem = transaction.msgSize - rcv;
/* the entire CAN frame might not be data, so we need to know
how many bytes are suppose to be left */
let min = 7;
if(rem < 7)
min = rem;
if(transaction.state === J1939TP_STATE_WAIT_DATA) {
//console.log( 'DT: ', msg.buf[0], 'of', transaction.numPackets );
if(transaction.nextSeq === msg.buf[0] && (rcv + min) <= J1939CFG_TP_RX_BUF_SIZE && (rcv + rem) === transaction.msgSize) {
if(transaction.timer)
clearTimeout(transaction.timer);
}
transaction.timer = setTimeout(this.onTpTimeout.bind(this, transaction), J1939TP_TIMEOUT_T1);
msg.buf.copy(transaction.buf, rcv, 1, 1 + min);
if(transaction.nextSeq++ >= transaction.numPackets) {
// that was the last packet - receive the message and clean up
this.emit('data', {
pri: transaction.pri,
src: transaction.src,
dst: transaction.dst,
pgn: transaction.pgn,
buf: transaction.buf,
});
this.sendAck(transaction);
this.completeTpTransaction(transaction);
} else {
// not the last packet, if we need to send a CTS, do it
if(++transaction.ctsCnt >= transaction.ctsMax) {
this.sendCts(transaction);
}
}
} else {
transaction.rsn = J1939TP_RSN_RSRCS_FREED;
this.sendAbort(transaction.src, transaction);
this.completeTpTransaction(transaction);
}
}
processDt(msg) {
//console.log('DT from ', msg.src);
let transaction = this.findRxTransaction(msg.dst, msg.src, [J1939TP_STATE_WAIT_DATA]);
if(transaction) {
if(msg.dst === J1939_ADDR_GLOBAL) {
this.rxBam(transaction, msg);
} else {
this.rxRtsCts(transaction, msg);
}
}
}
/**
* Updates the state of a TX BAM message and sends out the next packet
*
* @param {Object} transaction The transaction
*/
updateTxBam(transaction) {
// it would be better if we handled packet send failures, and
// an overall timeout: if we have not sent any packets in 200ms,
// fail the whole message.
if(this.sendDt(transaction)) {
if(transaction.numCur >= transaction.numPackets) {
this.completeTpTransaction(transaction);
} else {
transaction.numCur++;
transaction.timer = setTimeout(this.updateTxBam.bind(this, transaction), this.bamInterval);
}
}
}
/**
* Send a J1939 Connection management: abort message
*
* @param transaction The transaction to abort
*/
sendAbort(dst, transaction) {
//console.log('ABORT to ', dst);
var buf = Buffer.from([
J1939TP_CTRL_BYTE_ABORT,
transaction.rsn,
0xFF,
0xFF,
0xFF,
transaction.pgn & 0xFF,
(transaction.pgn >> 8) & 0xFF,
(transaction.pgn >> 16) & 0xFF
]);
this.write({ pgn: J1939_PGN_TP_CM, dst: dst, buf: buf });
}
/**
* Send a J1939 Connection management: ack message
*
* @param transaction The transaction to abort
*/
sendAck(transaction) {
//console.log('ACK to ', transaction.src);
var buf = Buffer.from([
J1939TP_CTRL_BYTE_ACK,
transaction.msgSize & 0xFF,
(transaction.msgSize >> 8) & 0xFF,
transaction.numPackets,
0xFF,
transaction.pgn & 0xFF,
(transaction.pgn >> 8) & 0xFF,
(transaction.pgn >> 16) & 0xFF
]);
this.write({ pgn: J1939_PGN_TP_CM, dst: transaction.src, buf: buf });
}
/**
* Send J1939 Transport Protocol RTS or BAM
*
* This kicks off a multi-packet transfer
*
* @param {<type>} transaction The transaction
* @param {<type>} type The type (BAM or RTS)
*/
sendBamOrRts(transaction, type) {
//console.log('RTS/BAM to ', transaction.dst);
var buf = Buffer.from([
type,
transaction.msgSize & 0xFF,
(transaction.msgSize >> 8) & 0xFF,
transaction.numPackets,
0xFF,
transaction.pgn & 0xFF,
(transaction.pgn >> 8) & 0xFF,
(transaction.pgn >> 16) & 0xFF
]);
this.write({ pgn: J1939_PGN_TP_CM, dst: transaction.dst, buf: buf });
}
sendDt(transaction) {
//console.log('DT to ', transaction.dst);
let snt = (transaction.numCur - 1) * 7;
let rem = transaction.msgSize - snt;
var data = [transaction.numCur];
// insert the data, or pad with 255
for(let cnt = 0; cnt < 7; cnt++) {
data.push((rem > cnt) ? transaction.buf[snt + cnt] : 255);
}
this.write({ pgn: J1939_PGN_TP_DT, dst: transaction.dst, buf: Buffer.from(data) });
// always indicate success cuz we don't know better
return true;
}
/**
* Sends a CTS transport protocol frame
*
* @param {<type>} transaction The transaction
*/
sendCts(transaction) {
//console.log('CTS to ', transaction.src);
var buf = Buffer.from([
J1939TP_CTRL_BYTE_CTS,
transaction.ctsMax,
transaction.nextSeq,
0xFF,
0xFF,
transaction.pgn & 0xFF,
(transaction.pgn >> 8) & 0xFF,
(transaction.pgn >> 16) & 0xFF
]);
this.write({ pgn: J1939_PGN_TP_CM, dst: transaction.src, buf: buf });
}
/**
* Looks up a transmit J1939 transaction
*
* @param dst The destination
* @param src The source
* @param pgn The pgn
* @return Object if found, null otherwise
*/
findTxTransaction(dst, src, pgn, states) {
let index = this.transactions.findIndex(function(item) {
return item.type === 'tx' && item.dst === dst && item.src === src && item.pgn === pgn && states.indexOf(item.state) > -1;
});
return (index > -1) ? this.transactions[index] : null;
}
/**
* Looks up a receive J1939 transaction
*
* @param dst The destination
* @param src The source
* @param pgn The pgn
* @return Object if found, null otherwise
*/
findRxTransaction(dst, src, states) {
let index = this.transactions.findIndex(function(item) {
return item.type === 'rx' && item.dst === dst && item.src === src && states.indexOf(item.state) > -1;
});
return (index > -1) ? this.transactions[index] : null;
}
/**
* If there is a matching RX transactioon, kills it
*/
cleanRxTransaction(dst, src) {
let transaction = this.findRxTransaction(dst, src, [J1939TP_STATE_SEND_ABORT, J1939TP_STATE_WAIT_DATA, J1939TP_STATE_WAIT_ACK]);
if(transaction) {
this.completeTpTransaction(transaction);
}
}
/**
* Find and remove the transaction from our queue
*
*
* @param {Object} transaction The transaction
*/
completeTpTransaction(transaction) {
let index = this.transactions.findIndex(function(item) {
return item === transaction;
});
if(index > -1) {
if(transaction.timer) {
clearTimeout(transaction.timer);
}
delete this.transactions.splice(index, 1);
}
}
/**
* Handler for timeout waiting for J1939 Transport Protocol response
*/
onTpTimeout(transaction) {
if(transaction.type === 'tx') {
if(transaction.state !== J1939TP_STATE_SEND_RTS) {
transaction.rsn = J1939TP_RSN_TIMEOUT;
transaction.state = J1939TP_STATE_SEND_ABORT;
this.sendAbort(transaction.dst, transaction);
}
} else {
if(J1939TP_STATE_WAIT_DATA === transaction.state) {
transaction.rsn = J1939TP_RSN_TIMEOUT;
transaction.state = J1939TP_STATE_SEND_ABORT;
this.sendAbort(transaction.src, transaction);
}
}
if(transaction.cb) {
transaction.cb(new Error('Timeout'));
}
this.completeTpTransaction(transaction);
}
/**
* Build a CAN 29-bit ID for J1939 PGN
*
* @param {number} pgn The pgn
* @param {number} to destination
* @param {number} pri The priority
* @return {number} The identifier.
*/
buildId(pgn, to, pri) {
pri = pri || 4;
var pf = (pgn >> 8) & 0xFF;
// pgn includes dp and edp bits
if(pf < 240) {
return (pri << 26) + (pgn << 8) + (to << 8) + this.address;
} else {
return (pri << 26) + (pgn << 8) + this.address;
}
}
};