UNPKG

@csllc/j1939

Version:

J1939 transport layer for CANBUS communication

1,137 lines (881 loc) 33.5 kB
/** * 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; } } };