UNPKG

@kybarg/ebds

Version:

Node.js package to work with bill acceptors using EBDS protocol. Linux, OSX and Windows.

352 lines (312 loc) 11.8 kB
const states = require('./states'); const commands = require('./commands'); const models = require('./models'); const currencies = require('./currencies'); // function bitGet(number, bitPosition) { // return (number & (1 << bitPosition)) === 0 ? 0 : 1; // } function bitTest(num, bit) { return (num >> bit) % 2 !== 0; } function bitSet(num, bit) { return num | (1 << bit); } function bitClear(num, bit) { return num & ~(1 << bit); } // function bitToggle(num, bit) { // return bit_test(num, bit) ? bit_clear(num, bit) : bit_set(num, bit); // } function bitExtract(num, count, position) { return ((1 << count) - 1) & (num >> position); } // The checksum is calculated on all bytes between the STX and the ETX (excluding the STX and the ETX) function calculateChecksum(bufer) { return [...bufer].reduce((accum, value) => accum ^ value, 0); } function readField16(bufer, field) { return parseInt( bufer .slice(field * 4, field * 4 + 4) .reduce((accum, val) => accum + (val & 0x0f).toString(16), ''), 16 ); } function readField32(bufer, field) { return parseInt( bufer .slice(field * 8, field * 8 + 8) .reduce((accum, val) => accum + (val & 0x0f).toString(32), ''), 16 ); } // function hex_to_ascii(str1) { // var hex = str1.toString(); // var str = ''; // for (var n = 0; n < hex.length; n += 2) { // str += String.fromCharCode(parseInt(hex.substr(n, 2), 16)); // } // return str; // } function parse(bufer, currentCommand, currency = 'USD') { // const STX = bufer[0]; const LENGTH = bufer[1]; const CTL = bufer[2]; let DATA = bufer.slice(3, LENGTH - 2); // const ETX = bufer[9]; const CHEHCKSUM = bufer[bufer.length - 1]; const MESSAGE_TYPE = (CTL & 0xf0) >> 4; // const ACK_NUMBER = (CTL >> 0) & 1; let result = { success: true, command: currentCommand, info: {}, }; // if (ACK_NUMBER !== ackNumber) { // result.success = false; // result.info.error = 'ACK_ERROR'; // result.info.message = 'Ack number is wrong, try to send command again'; // return result; // } // const command = commands[currentCommand]; const checksum = calculateChecksum([LENGTH, CTL].concat([...DATA])); if (CHEHCKSUM !== checksum) { result.success = false; result.info.error = 'CHEHCKSUM_ERROR'; result.info.message = 'Checksum is failed'; return result; } // if (LENGTH !== command.responseLength) { // result.success = false; // result.info.error = 'Invalid response length'; // return result; // } if (MESSAGE_TYPE === 2 || MESSAGE_TYPE === 7) { if (MESSAGE_TYPE === 7) { DATA = DATA.slice(1, DATA.length); } result.info = { idling: bitTest(DATA[0], 0), accepting: bitTest(DATA[0], 1), escrowed: bitTest(DATA[0], 2), stacking: bitTest(DATA[0], 3), stacked: bitTest(DATA[0], 4), returning: bitTest(DATA[0], 5), returned: bitTest(DATA[0], 6), cheated: bitTest(DATA[1], 0), rejected: bitTest(DATA[1], 1), jammed: bitTest(DATA[1], 2), cassetteFull: bitTest(DATA[1], 3), lrcInstalled: bitTest(DATA[1], 4), paused: bitTest(DATA[1], 5), calibration: bitTest(DATA[1], 6), powerUp: bitTest(DATA[2], 0), invalidCommand: bitTest(DATA[2], 1), failure: bitTest(DATA[2], 2), billValue: currencies[currency][bitExtract(DATA[2], 3, 3)], stalled: bitTest(DATA[3], 0), flashDownload: bitTest(DATA[3], 1), prestack: bitTest(DATA[3], 2), barcodeSupport: bitTest(DATA[3], 3), allowsDeviceCaps: bitTest(DATA[3], 4), model: models[DATA[4]] || DATA[4], codeRevision: DATA[5], }; if (MESSAGE_TYPE === 7) { const EXPANDED_DATA = Buffer.from(DATA.slice(6, DATA.length)); result.info.expanded = { index: parseInt(EXPANDED_DATA[0], 16), isoCode: EXPANDED_DATA.slice(1, 4).toString('ascii'), baseValue: parseInt(EXPANDED_DATA.slice(4, 7).toString('ascii')), sign: EXPANDED_DATA.slice(7, 8).toString('ascii'), exponent: parseInt(EXPANDED_DATA.slice(8, 10).toString('ascii')), orientation: parseInt(EXPANDED_DATA[10], 16), type: EXPANDED_DATA.slice(11, 12).toString('ascii'), series: EXPANDED_DATA.slice(12, 13).toString('ascii'), compatibility: EXPANDED_DATA.slice(13, 14).toString('ascii'), version: EXPANDED_DATA.slice(14, 15).toString('ascii'), }; } const resultStates = states.filter( ({ byte, bit, negative }) => bitTest(DATA[byte], bit) === !negative ); result.info.statuses = resultStates.map( ({ name, description, denomination, type }) => { const res = { name, description, type }; if (denomination) { res.info = { denomination: currencies[currency][bitExtract(DATA[2], 3, 3)], currency, }; if (MESSAGE_TYPE === 7) { let denomination = result.info.expanded.baseValue * Math.pow(10, result.info.expanded.exponent); if (result.info.expanded.sign === '-') denomination = result.info.expanded.baseValue / Math.pow(10, result.info.expanded.exponent); res.info = { denomination, currency, }; } } return res; } ); } if (MESSAGE_TYPE === 6) { if (currentCommand === 'SOFTWARE_CRC') { result.info.value = DATA.slice(0, 4).reduce( (accum, val) => accum + (val & 0x0f).toString(16), '0x' ); } else if (currentCommand === 'CLEAR_CASH_VALUE_IN_CASSETTE') { result.info.value = parseInt( DATA.reduce((accum, val) => accum + (val & 0x0f).toString(24), ''), 16 ); } else if ( [ 'ACCEPTOR_SERIAL_NUMBER', 'ACCEPTOR_BOOT_SOFTWARE_VERSION', 'ACCEPTOR_APPLICATION_SOFTWARE_VERSION', 'ACCEPTOR_VARIANT_VERSION', 'ACCEPTOR_VARIANT_NAME', 'ACCEPTOR_TYPE', ].includes(currentCommand) ) { result.info.value = Buffer.from(DATA) .toString() .replace(/[^\x20-\x7E]/g, ''); } else if ( ['CASH_VALUE_IN_CASSETTE', 'NUMBER_OF_ACCEPTOR_RESETS'].includes( currentCommand ) ) { result.info.value = DATA.reduce((accum, val) => accum + (val & 0x0f), 0); } else if (currentCommand === 'ACCEPTOR_AUDIT_LIFE_TIME_TOTALS') { result.info = { performanceDataMapID: readField32(DATA, 0), totalOperatingHours: readField32(DATA, 1), totalMotorStarts: readField32(DATA, 2), totalDocumentsReachedEscrowPosition: readField32(DATA, 3), totalDocumentsPassedRecognition: readField32(DATA, 4), totalDocumentsPassedValidation: readField32(DATA, 5), }; } else if (currentCommand === 'ACCEPTOR_AUDIT_QP_MEASURES') { result.info = { last100BillsAcceptRate: readField16(DATA, 0), totalMotorStarts: readField16(DATA, 1), totalDocumentsStacked: readField16(DATA, 2), totalDocumentsReachedEscrowPosition: readField16(DATA, 3), totalDocumentsPassedRecognition: readField16(DATA, 4), totalDocumentsPassedValidation: readField16(DATA, 5), totalRecognitionRejections: readField16(DATA, 6), totalSecurityRejections: readField16(DATA, 7), totalOrientationDisabledRejections: readField16(DATA, 8), totalDocumentDisabledRejections: readField16(DATA, 9), totalFastFeedRejectionRejections: readField16(DATA, 10), totalDocumentsInsertedwhileDisabled: readField16(DATA, 11), totalHostReturnDocumentRejections: readField16(DATA, 12), totalBarcodesDecoded: readField16(DATA, 13), }; } else if ( currentCommand === 'ACCEPTOR_AUDIT_GENERAL_PERFORMANCE_MEASURES' ) { result.info = { totalCrossChannel0Rejects: readField16(DATA, 0), totalCrossChannel1Rejects: readField16(DATA, 1), totalSumofAllJams: readField16(DATA, 2), totalJamRecoveryEfforts: readField16(DATA, 3), totalRejectAttemptsFollowedbyJam: readField16(DATA, 4), totalStackerJams: readField16(DATA, 5), totalJamswithoutRecoveryEnabled: readField16(DATA, 6), totalOutOfServiceConditions: readField16(DATA, 7), totalOutOfOrderConditions: readField16(DATA, 8), totalOperatingHours: readField16(DATA, 9), totalDocumentsExceedingMaxLength: readField16(DATA, 10), totalDocumentsunderMinLength: readField16(DATA, 11), totalDocumentsFailedToReachEscrowPosition: readField16(DATA, 12), totalCalibrations: readField16(DATA, 13), totalPowerups: readField16(DATA, 14), totalDownloadAttempts: readField16(DATA, 15), totalCassettesFull: readField16(DATA, 16), totalCassettesRemoved: readField16(DATA, 17), }; } } // 02 1e 70 02 01 10 00 10 55 35 01 55 53 44 30 30 31 2b 30 30 00 43 41 44 42 00 00 00 03 50 if (result.info.invalidCommand || result.info.failure) { result.success = false; } return result; } // STX LENGTH MSG_TYPE DATA ETX CHECK_SUM function compose(commandName, args = {}, ACK_NUMBER = 0) { const { data, type, subtype } = commands[commandName]; let DATA = data; if (type === 0x10 || type === 0x70) { args.denominations.forEach((set, bit) => { if (set) DATA[0] = bitSet(DATA[0], bit); }); if (args.specialInterruptMode) DATA[1] = bitSet(DATA[1], 0); if (args.highSecurity) DATA[1] = bitSet(DATA[1], 1); if (args.orientation) { switch (args.orientation) { case 1: DATA[1] = bitClear(DATA[1], 2); DATA[1] = bitClear(DATA[1], 3); break; case 2: DATA[1] = bitSet(DATA[1], 2); DATA[1] = bitClear(DATA[1], 3); break; case 4: DATA[1] = bitSet(DATA[1], 2); DATA[1] = bitSet(DATA[1], 3); break; } } if (args.escrowMode) DATA[1] = bitSet(DATA[1], 4); if (args.noPushMode) DATA[2] = bitSet(DATA[2], 0); if (args.powerUpPolicy === 'B') { bitSet(DATA[2], 2); } else if (args.powerUpPolicy === 'C') { bitSet(DATA[2], 3); } if (args.expandedNoteReporting) DATA[2] = bitSet(DATA[2], 4); if (args.expandedCouponReporting) DATA[2] = bitSet(DATA[2], 5); if (commandName === 'ENABLE') { if (args.denominations.includes(1)) { args.denominations.forEach((set, bit) => { if (set) DATA[0] = bitSet(DATA[0], bit); }); } else DATA[0] = 0x7f; if (args.enableBarcode) DATA[2] = bitSet(DATA[2], 1); } else if (commandName === 'DISABLE') { DATA[0] = 0x00; DATA[2] = bitClear(DATA[2], 1); } else if (commandName === 'STACK') { DATA[1] = bitSet(DATA[1], 5); } else if (commandName === 'RETURN') { DATA[1] = bitSet(DATA[1], 6); } } if (type === 0x70) { DATA = [subtype, ...DATA]; if (subtype === 0x02) { DATA = [...DATA, args.index]; } } const STX = 0x02; const ETX = 0x03; const LENGTH = 5 + DATA.length; // STX+LENGTH+MSG_TYPE+ETX+CHECKSUM=5 + DATA const MSG_TYPE = type | (ACK_NUMBER & 0xf); const buffer = [STX, LENGTH, MSG_TYPE].concat(DATA, ETX); const CHECKSUM = calculateChecksum(buffer.slice(1, -1)); buffer.push(CHECKSUM); return Buffer.from(buffer); } module.exports = { parse, calculateChecksum, compose };