├── .gitignore ├── README.md ├── examples ├── card_scan.js ├── mifare_classic.js ├── read_data.js └── write_data.js ├── package.json └── src ├── constants.js ├── frame.js ├── frame_emitter.js ├── logs.js ├── pn532.js ├── pn532_i2c.js └── pn532_uart.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | !dist 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PN532 2 | 3 | Driver for the PN532 NFC chip. Provides an event and promise-based API, and requires either: 4 | - [node-serialport](https://github.com/voodootikigod/node-serialport) 5 | - [node-i2c](https://github.com/kelly/node-i2c) (WIP) 6 | 7 | This implementation does not require libnfc, and should work on both X86 (32-bit or 64-bit) and ARM (RPi / Beaglebone) systems 8 | 9 | Tested on a Mac OSX 10.9 system using a UART/FTDI cable to an [Adafruit breakout board](https://www.adafruit.com/products/364) 10 | and on a BeagleBone using UART. I2C support is currently a WIP at the moment. 11 | 12 | API is subject to change until the 1.0.0 release 13 | 14 | ### Install 15 | npm install pn532 16 | 17 | and `npm install serialport` or `npm install i2c` 18 | 19 | ### Example 20 | 21 | #### UART (using [node-serialport](https://github.com/voodootikigod/node-serialport)) 22 | ```js 23 | var pn532 = require('pn532'); 24 | var SerialPort = require('serialport'); 25 | 26 | var serialPort = new SerialPort({path:'/dev/tty.usbserial-AFWR836M', baudRate: 115200 }); 27 | var rfid = new pn532.PN532(serialPort); 28 | ``` 29 | 30 | #### I2C (using [node-i2c](https://github.com/kelly/node-i2c)) 31 | ```js 32 | var pn532 = require('pn532'); 33 | var i2c = require('i2c'); 34 | 35 | var wire = new i2c(pn532.I2C_ADDRESS, {device: '/dev/i2c-1'}); 36 | var rfid = new pn532.PN532(wire); 37 | ``` 38 | 39 | #### Scan a tag 40 | ```js 41 | rfid.on('ready', function() { 42 | rfid.scanTag().then(function(tag) { 43 | if (tag) console.log('tag:', tag.uid); 44 | }); 45 | }); 46 | ``` 47 | 48 | #### Poll for a tag 49 | ```js 50 | rfid.on('ready', function() { 51 | console.log('Listening for a tag scan...'); 52 | rfid.on('tag', function(tag) { 53 | if (tag) console.log('tag:', tag.uid); 54 | }); 55 | }); 56 | ``` 57 | 58 | #### Retrieve the firmware version 59 | ```js 60 | rfid.on('ready', function() { 61 | rfid.getFirmwareVersion().then(function(data) { 62 | console.log('firmware: ', data); 63 | }); 64 | }); 65 | ``` 66 | 67 | ### Read and write tag data (using [ndef library](https://www.npmjs.com/package/ndef)) 68 | Tested using NTAG203 tags. Should support other NTAG and Mifare Ultralight tags. Mifare Classic tags are currently NOT supported, but could be in the future. 69 | 70 | #### Read 71 | ```js 72 | rfid.on('ready', function() { 73 | rfid.on('tag', function(tag) { 74 | rfid.readNdefData().then(function(data) { 75 | var records = ndef.decodeMessage(Array.from(data)); 76 | console.log(records); 77 | }); 78 | }); 79 | }); 80 | ``` 81 | #### Write 82 | ```js 83 | rfid.on('ready', function() { 84 | rfid.scanTag().then(function(tag) { 85 | var messages = [ 86 | ndef.uriRecord('http://www.google.com'), 87 | ndef.textRecord('test') 88 | ]; 89 | var data = ndef.encodeMessage(messages); 90 | 91 | rfid.writeNdefData(data).then(function(response) { 92 | console.log('Write successful'); 93 | }); 94 | }); 95 | }); 96 | ``` 97 | 98 | ### Examples 99 | Examples are available under the `examples` directory 100 | 101 | ### Debug logging 102 | `PN532_LOGGING=debug node examples/card_scan.js` 103 | 104 | ### Note for using UART on a Raspberry Pi 3 105 | If you are using this library on a Raspberry Pi 3, you will likely encounter an [issue](https://github.com/techniq/node-pn532/issues/9) with the device sending or receiving data over UART due to some hardware and configuration changes with regards to the serial port. 106 | 107 | TLDR workaround: 108 | 1. Add `core_freq=250` in the `/boot/cmdline.txt` 109 | 2. Use `/dev/ttyS0` instead of `/dev/ttyAMA0` 110 | 111 | For details on why these changes are needed, see [here](http://elinux.org/RPi_Serial_Connection#Preventing_Linux_using_the_serial_port) and [here](https://blog.adafruit.com/2016/03/07/raspberry-pi-3-uart-speed-workaround/) 112 | 113 | ### Links 114 | - [Datasheet](http://www.nxp.com/documents/short_data_sheet/PN532_C1_SDS.pdf) 115 | - [User manual](http://www.nxp.com/documents/user_manual/141520.pdf) 116 | -------------------------------------------------------------------------------- /examples/card_scan.js: -------------------------------------------------------------------------------- 1 | var pn532 = require('../src/pn532'); 2 | var SerialPort = require('serialport'); 3 | 4 | var serialPort = new SerialPort('/dev/tty.usbserial-AFWR836M', { baudRate: 115200 }); 5 | var rfid = new pn532.PN532(serialPort); 6 | 7 | console.log('Waiting for rfid ready event...'); 8 | rfid.on('ready', function() { 9 | console.log('Listening for a tag scan...'); 10 | rfid.on('tag', function(tag) { 11 | console.log(Date.now(), 'UID:', tag.uid); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /examples/mifare_classic.js: -------------------------------------------------------------------------------- 1 | var pn532 = require('../src/pn532'); 2 | var SerialPort = require('serialport'); 3 | 4 | var serialPort = new SerialPort('/dev/tty.usbserial-AFWR836M', { baudRate: 115200 }); 5 | var rfid = new pn532.PN532(serialPort, { pollInterval: 3000 }); 6 | var ndef = require('ndef'); 7 | 8 | console.log('Waiting for rfid ready event...'); 9 | rfid.on('ready', function() { 10 | 11 | console.log('Listening for a tag scan...'); 12 | rfid.on('tag', function(tag) { 13 | console.log('Tag', tag); 14 | 15 | console.log('Authenticating...'); 16 | rfid.authenticateBlock(tag.uid).then(function() { 17 | console.log('Reading tag data...'); 18 | rfid.readData().then(function(data) { 19 | console.log('Tag data:', data); 20 | 21 | var records = ndef.decodeMessage(data.toJSON()); 22 | console.log(records); 23 | }); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /examples/read_data.js: -------------------------------------------------------------------------------- 1 | var pn532 = require('../src/pn532'); 2 | var SerialPort = require('serialport'); 3 | 4 | var serialPort = new SerialPort('/dev/tty.usbserial-AFWR836M', { baudRate: 115200 }); 5 | var rfid = new pn532.PN532(serialPort); 6 | var ndef = require('ndef'); 7 | 8 | console.log('Waiting for rfid ready event...'); 9 | rfid.on('ready', function() { 10 | 11 | console.log('Listening for a tag scan...'); 12 | rfid.on('tag', function(tag) { 13 | console.log('Tag', tag); 14 | 15 | console.log('Reading tag data...'); 16 | rfid.readNdefData().then(function(data) { 17 | console.log('Tag data:', data); 18 | 19 | var records = ndef.decodeMessage(Array.from(data)); 20 | console.log(records); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /examples/write_data.js: -------------------------------------------------------------------------------- 1 | var pn532 = require('../src/pn532'); 2 | var SerialPort = require('serialport'); 3 | 4 | var serialPort = new SerialPort('/dev/tty.usbserial-AFWR836M', { baudRate: 115200 }); 5 | var rfid = new pn532.PN532(serialPort); 6 | var ndef = require('ndef'); 7 | 8 | console.log('Waiting for rfid ready event...'); 9 | rfid.on('ready', function() { 10 | 11 | console.log('Waiting for a tag...'); 12 | rfid.scanTag().then(function(tag) { 13 | console.log('Tag found:', tag); 14 | 15 | var messages = [ 16 | ndef.uriRecord('http://www.google.com'), 17 | ndef.textRecord('test') 18 | ]; 19 | var data = ndef.encodeMessage(messages); 20 | 21 | console.log('Writing tag data...'); 22 | rfid.writeNdefData(data).then(function(response) { 23 | console.log('Write successful'); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pn532", 3 | "version": "1.0.0", 4 | "description": "Library to run the PN532 RFID Reader", 5 | "repository": "https://github.com/techniq/node-pn532", 6 | "author": "Sean Lynch ", 7 | "license": "MIT", 8 | "main": "src/pn532.js", 9 | "devDependencies": {}, 10 | "dependencies": { 11 | "winston": "^0.8.0" 12 | }, 13 | "engine": { 14 | "node": ">=8.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* 3 | PN532 User Manual 4 | */ 5 | 6 | // Typical PN532 address 7 | exports.I2C_ADDRESS = 0x48 >> 1; // 7-bit address bit shifted to 8-bit (?) 8 | // exports.I2C_READBIT = 0x01; 9 | // exports.I2C_BUSY = 0x00; 10 | // exports.I2C_READY = 0x01; 11 | // exports.I2C_READYTIMEOUT = 20; 12 | 13 | // Section 7 - Commands supported (page 65) 14 | // Miscellaneous 15 | exports.COMMAND_DIAGNOSE = 0x00; 16 | exports.COMMAND_GET_FIRMWARE_VERSION = 0x02; 17 | exports.COMMAND_GET_GENERAL_STATUS = 0x04; 18 | exports.COMMAND_READ_REGISTER = 0x06; 19 | exports.COMMAND_WRITE_REGISTER = 0x08; 20 | exports.COMMAND_READ_GPIO = 0x0C; 21 | exports.COMMAND_WRITE_GPIO = 0x0E; 22 | exports.COMMAND_SET_SERIAL_BAUD_RATE = 0x10; 23 | exports.COMMAND_SET_PARAMETERS = 0x12; 24 | exports.COMMAND_SAMCONFIGURATION = 0x14; 25 | exports.COMMAND_POWER_DOWN = 0x16; 26 | // RF Communicaions 27 | exports.COMMAND_RF_CONFIGUATION = 0x32; 28 | exports.COMMAND_RF_REGULATION_TEST = 0x58; 29 | // Initiator 30 | exports.COMMAND_IN_JUMP_FOR_DEP = 0x56; 31 | exports.COMMAND_IN_JUMP_FOR_PSL = 0x46; 32 | exports.COMMAND_IN_LIST_PASSIVE_TARGET = 0x4A; 33 | exports.COMMAND_IN_ATR = 0x50; 34 | exports.COMMAND_IN_PSL = 0x4E; 35 | exports.COMMAND_IN_DATA_EXCHANGE = 0x40; 36 | exports.COMMAND_IN_COMMUNICATE_THRU = 0x42; 37 | exports.COMMAND_IN_DESELECT = 0x44; 38 | exports.COMMAND_IN_RELEASE = 0x52; 39 | exports.COMMAND_IN_SELECT = 0x54; 40 | exports.COMMAND_IN_AUTO_POLL = 0x60; 41 | // Target 42 | exports.TG_INIT_AS_TARGET = 0x8C; 43 | exports.TG_SET_GENERAL_BYTES = 0x92; 44 | exports.TG_GET_DATA = 0x86; 45 | exports.TG_SET_DATA = 0x8E; 46 | exports.TG_SET_META_DATA = 0x94; 47 | exports.TG_GET_INITIATOR_COMMAND = 0x88; 48 | exports.TG_RESPONSE_TO_INITIATOR = 0x90; 49 | exports.TG_GET_TARGET_STATUS = 0x8A; 50 | 51 | // Frame Identifiers (TFI) 52 | exports.DIRECTION_HOST_TO_PN532 = 0xD4; 53 | exports.DIRECTION_PN532_TO_HOST = 0xD5; 54 | 55 | // Values for PN532's SAMCONFIGURATION function. 56 | exports.SAMCONFIGURATION_MODE_NORMAL = 0x01; 57 | exports.SAMCONFIGURATION_MODE_VIRTUAL_CARD = 0x02; 58 | exports.SAMCONFIGURATION_MODE_WIRED_CARD = 0x03; 59 | exports.SAMCONFIGURATION_MODE_DUAL_CARD = 0X04; 60 | 61 | exports.SAMCONFIGURATION_TIMEOUT_50MS = 0x01; 62 | 63 | exports.SAMCONFIGURATION_IRQ_OFF = 0x00; 64 | exports.SAMCONFIGURATION_IRQ_ON = 0x01; 65 | 66 | // Values for the PN532's RFCONFIGURATION function. 67 | exports.RFCONFIGURATION_CFGITEM_MAXRETRIES = 0x05; 68 | 69 | // Section 7.3.5 (page 115) 70 | exports.CARD_ISO14443A = 0x00; // 106 kbps type A (ISO/IEC14443 Type A) 71 | exports.CARD_FELICA212 = 0x01; // 212 kbps (FeliCa polling) 72 | exports.CARD_FELICA414 = 0x02; // 424 kbps (FeliCa polling) 73 | exports.CARD_ISO14443B = 0x03; // 106 kbps type B (ISO/IEC14443-3B) 74 | exports.CARD_JEWEL = 0x04; // 106 kbps Innovision Jewel tag 75 | 76 | exports.MIFARE_COMMAND_AUTH_A = 0x60; 77 | exports.MIFARE_COMMAND_AUTH_B = 0x61; 78 | exports.MIFARE_COMMAND_READ = 0x30; 79 | exports.MIFARE_COMMAND_WRITE_4 = 0xA2; 80 | exports.MIFARE_COMMAND_WRITE_16 = 0xA0; 81 | 82 | exports.TAG_MEM_NULL_TLV = 0x00; 83 | exports.TAG_MEM_LOCK_TLV = 0x01; 84 | exports.TAG_MEM_MEMCONTROL_TLV = 0x02; 85 | exports.TAG_MEM_NDEF_TLV = 0x03; 86 | exports.TAG_MEM_PROPRIETARY_TLV = 0xFD; 87 | exports.TAG_MEM_TERMINATOR_TLV = 0xFE; 88 | -------------------------------------------------------------------------------- /src/frame.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var util = require('util'); 3 | 4 | var c = require('./constants'); 5 | 6 | var PREAMBLE = 0x00; 7 | var START_CODE_1 = 0x00; 8 | var START_CODE_2 = 0xFF; 9 | var POSTAMBLE = 0x00; 10 | 11 | /* 12 | Represents a single communication frame for communication with the PN532 NFC Chip. 13 | */ 14 | class Frame { 15 | // Gets the frame's data length 16 | getFrameLength() { 17 | throw new Error('Implement in subclass'); 18 | } 19 | 20 | // Convert Frame instance to a Buffer instance 21 | toBuffer() { 22 | throw new Error('Implement in subclass'); 23 | } 24 | 25 | [util.inspect.custom]() { 26 | return util.format(''); 27 | } 28 | 29 | static fromBuffer(buffer) { 30 | if (AckFrame.isFrame(buffer)) { 31 | return new AckFrame(); 32 | } 33 | 34 | if (NackFrame.isFrame(buffer)) { 35 | return new NackFrame(); 36 | } 37 | 38 | if (ErrorFrame.isFrame(buffer)) { 39 | return new ErrorFrame(); 40 | } 41 | 42 | if (DataFrame.isFrame(buffer)) { 43 | return new DataFrame(buffer); 44 | } 45 | 46 | throw new Error('Invalid Response'); 47 | } 48 | 49 | static isFrame(buffer) { 50 | return DataFrame.isFrame(buffer) || 51 | AckFrame.isFrame(buffer) || 52 | NackFrame.isFrame(buffer) || 53 | ErrorFrame.isFrame(buffer); 54 | } 55 | } 56 | 57 | class DataFrame extends Frame { 58 | /* 59 | Constructor 60 | 61 | @param {(bufer|array}} data - Complete packet data, or just the data portion of the package data 62 | @param {number} [direction=DIRECTION_HOST_TO_PN532] - Way of message (PN532 to HOST, or HOST to PN532. See DIRECTION_* constants) 63 | */ 64 | constructor(data, direction) { 65 | super(); 66 | if (data instanceof Buffer) { 67 | var buffer = data; 68 | 69 | var dataLength = buffer[3]; 70 | var dataStart = 6; 71 | var dataEnd = dataStart + dataLength; 72 | var d = new Buffer(dataLength); 73 | buffer.copy(d, 0, dataStart, dataEnd); 74 | 75 | this._data = d; 76 | this.direction = buffer[5]; 77 | } else if (data instanceof Array) { 78 | this._data = data; 79 | this._direction = direction || c.DIRECTION_HOST_TO_PN532; 80 | } else { 81 | throw new Error('data must be an instanceof a Buffer or Array'); 82 | } 83 | } 84 | 85 | toJSON() { 86 | return { 87 | direction: this.getDirection(), 88 | data: { 89 | command: this.getDataCommand(), 90 | body: this.getDataBody(), 91 | // checksum: this.getDataChecksum(), 92 | // length: this.getDataLength(), 93 | // lengthChecksum: this.getDataLengthChecksum() 94 | }, 95 | }; 96 | } 97 | 98 | [util.inspect.custom]() { 99 | return util.format('', this.toJSON()); 100 | } 101 | 102 | // Gets the frame's direction 103 | getDirection() { 104 | return this._direction; 105 | } 106 | 107 | // Gets the frame's data 108 | getData() { 109 | return this._data; 110 | } 111 | 112 | getDataCommand() { 113 | return this._data[0]; 114 | } 115 | 116 | getDataBody() { 117 | return this._data.slice(1); 118 | } 119 | 120 | // Gets the frame's data length 121 | getDataLength() { 122 | return this._data.length + 1; 123 | } 124 | 125 | // Gets the checksum of getDataLength(). 126 | getDataLengthChecksum() { 127 | return (~this.getDataLength() & 0xFF) + 0x01; 128 | } 129 | 130 | // Gets a checksum for the frame's data. 131 | getDataChecksum() { 132 | var dataCopy = this._data.slice(); 133 | dataCopy.push(this._direction); 134 | 135 | var sum = dataCopy.reduce((prev,current) => prev + current); 136 | var inverse = (~sum & 0xFF) + 0x01; 137 | 138 | if (inverse > 255) { 139 | inverse = inverse - 255; 140 | } 141 | 142 | return inverse; 143 | } 144 | 145 | getFrameLength() { 146 | var frameLengthMinusData = 7; 147 | return this._data.length + frameLengthMinusData; 148 | } 149 | 150 | static isFrame(buffer) { 151 | // Checks if a buffer from the PN532 is valid. 152 | var frameLengthMinusData = 7; 153 | if (buffer.length <= frameLengthMinusData) { 154 | return false; 155 | } 156 | 157 | var dataLength = buffer[3]; 158 | var validFrameLength = frameLengthMinusData + dataLength; 159 | if (buffer.length < validFrameLength) { 160 | return false; 161 | } 162 | 163 | // TODO: Check LCS and DCS checksums 164 | 165 | return (buffer[0] === PREAMBLE && 166 | buffer[1] === START_CODE_1 && 167 | buffer[2] === START_CODE_2 && 168 | buffer[validFrameLength - 1] === 0x00); 169 | } 170 | 171 | toBuffer() { 172 | var array = [].concat([ 173 | PREAMBLE, 174 | START_CODE_1, 175 | START_CODE_2, 176 | this.getDataLength(), 177 | this.getDataLengthChecksum(), 178 | this.getDirection() 179 | ], this._data, [ 180 | this.getDataChecksum(), 181 | POSTAMBLE 182 | ]); 183 | return new Buffer(array); 184 | } 185 | } 186 | 187 | class AckFrame extends Frame { 188 | constructor() { 189 | super(); 190 | } 191 | 192 | getFrameLength() { 193 | return 6; 194 | } 195 | 196 | static isFrame(buffer) { 197 | // Checks if the buffer is an ACK frame. [00 00 FF 00 FF 00] 198 | return (buffer.length >= 6 && 199 | buffer[0] === PREAMBLE && 200 | buffer[1] === START_CODE_1 && 201 | buffer[2] === START_CODE_2 && 202 | buffer[3] === 0x00 && 203 | buffer[4] === 0xFF && 204 | buffer[5] === POSTAMBLE); 205 | } 206 | 207 | toBuffer() { 208 | return new Buffer([ 209 | PREAMBLE, 210 | START_CODE_1, 211 | START_CODE_2, 212 | 0x00, 213 | 0xFF, 214 | POSTAMBLE 215 | ]); 216 | } 217 | 218 | [util.inspect.custom]() { 219 | return util.format('', this.toBuffer()); 220 | } 221 | } 222 | 223 | class NackFrame extends Frame { 224 | constructor() { 225 | super(); 226 | } 227 | 228 | getFrameLength() { 229 | return 6; 230 | } 231 | 232 | static isFrame(buffer) { 233 | // Checks if the buffer is an NACK frame. [00 00 FF FF 00 00] 234 | return (buffer.length >= 6 && 235 | buffer[0] === PREAMBLE && 236 | buffer[1] === START_CODE_1 && 237 | buffer[2] === START_CODE_2 && 238 | buffer[3] === 0xFF && 239 | buffer[4] === 0x00 && 240 | buffer[5] === POSTAMBLE); 241 | 242 | } 243 | 244 | toBuffer() { 245 | return new Buffer([ 246 | PREAMBLE, 247 | START_CODE_1, 248 | START_CODE_2, 249 | 0xFF, 250 | 0x00, 251 | POSTAMBLE 252 | ]); 253 | } 254 | 255 | [util.inspect.custom]() { 256 | return util.format('', this.toBuffer()); 257 | } 258 | } 259 | 260 | class ErrorFrame extends DataFrame { 261 | constructor() { 262 | super([0x7F]); 263 | } 264 | 265 | static isFrame(buffer) { 266 | // Checks if the buffer is an error frame. [00 00 FF 01 FF 7F 81 00] 267 | return (buffer.length >= 8 && 268 | buffer[0] === PREAMBLE && 269 | buffer[1] === START_CODE_1 && 270 | buffer[2] === START_CODE_2 && 271 | buffer[3] === 0x01 && // Packet length 272 | buffer[4] === 0xFF && // Packet length checksum 273 | buffer[5] === 0x7F && // Specific application level error code 274 | buffer[6] === 0x81 && // Packet data checksum 275 | buffer[7] === POSTAMBLE); 276 | } 277 | 278 | toBuffer() { 279 | return new Buffer([ 280 | PREAMBLE, 281 | START_CODE_1, 282 | START_CODE_2, 283 | 0x01, // Packet length 284 | 0xFF, // Packet length checksum 285 | 0x7F, // Specific application level error code 286 | 0x81, // Packet data checksum 287 | POSTAMBLE 288 | ]); 289 | } 290 | } 291 | 292 | exports.Frame = Frame; 293 | exports.DataFrame = DataFrame; 294 | exports.AckFrame = AckFrame; 295 | exports.NackFrame = NackFrame; 296 | exports.ErrorFrame = ErrorFrame; 297 | -------------------------------------------------------------------------------- /src/frame_emitter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var util = require('util'); 3 | var EventEmitter = require('events').EventEmitter; 4 | var logger = require('winston').loggers.get('frame-emitter'); 5 | 6 | var frame = require('./frame'); 7 | var Frame = frame.Frame; 8 | var DataFrame = frame.DataFrame; 9 | var ErrorFrame = frame.ErrorFrame; 10 | var AckFrame = frame.AckFrame; 11 | var NackFrame = frame.NackFrame; 12 | 13 | class FrameEmitter extends EventEmitter { 14 | /* 15 | @constructor 16 | @param {object} hal - An instance of PN532_UART or PN532_I2C 17 | */ 18 | constructor(hal) { 19 | super(); 20 | this.hal = hal; 21 | this.buffer = new Buffer(0); 22 | 23 | logger.debug('listening to data'); 24 | 25 | // console.dir(hal); 26 | this.hal.on('data', (data) => { 27 | logger.debug('Data received', util.inspect(data)); 28 | this.buffer = Buffer.concat([this.buffer, data]); 29 | this._processBuffer(); 30 | }); 31 | 32 | this.hal.on('error', (error) => { 33 | logger.error('Error on HAL', error); 34 | this.emit('error', error); 35 | }); 36 | } 37 | 38 | _processBuffer() { 39 | // TODO: filter garbage at front of buffer (anything not 0x00, 0x00, 0xFF at start?) 40 | 41 | logger.debug('Processing buffer', util.inspect(this.buffer)); 42 | 43 | if (Frame.isFrame(this.buffer)) { 44 | logger.debug('Frame found in buffer'); 45 | 46 | var frame = Frame.fromBuffer(this.buffer); 47 | // logger.info('Frame', frame.toString()); 48 | logger.info('Frame', util.inspect(frame)); 49 | this.emit('frame', frame); 50 | 51 | if (frame instanceof ErrorFrame) { 52 | logger.error('ErrorFrame found in buffer'); 53 | this.emit('error', frame); 54 | } else if (frame instanceof DataFrame) { 55 | logger.debug('DataFrame found in buffer'); 56 | this.emit('response', frame); 57 | } else if (frame instanceof AckFrame) { 58 | logger.debug('AckFrame found in buffer'); 59 | } else if (frame instanceof NackFrame) { 60 | logger.debug('NackFrame found in buffer'); 61 | } 62 | 63 | this.buffer = this.buffer.slice(frame.getFrameLength()); // strip off frame's data from buffer 64 | 65 | // If more data still on buffer, process buffer again, 66 | // otherwise next 'data' event on serial will process the buffer after more data is receive 67 | if (this.buffer.length) { 68 | this._processBuffer(); 69 | } 70 | } 71 | } 72 | } 73 | 74 | exports.FrameEmitter = FrameEmitter; 75 | -------------------------------------------------------------------------------- /src/logs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var winston = require('winston'); 3 | 4 | module.exports = function setupLogging(level) { 5 | level = level || 'warn'; 6 | 7 | // winston.loggers.options.transports = [ 8 | // new winston.transports.Console({ 9 | // level: 'debug', 10 | // colorize: 'true', 11 | // label: 'pn532' 12 | // }) 13 | // ]; 14 | 15 | winston.loggers.add('pn532', { 16 | console: { 17 | level: level, 18 | colorize: 'true', 19 | label: 'pn532' 20 | } 21 | }); 22 | 23 | winston.loggers.add('frame', { 24 | console: { 25 | level: level, 26 | colorize: 'true', 27 | label: 'frame' 28 | } 29 | }); 30 | 31 | winston.loggers.add('frame-emitter', { 32 | console: { 33 | level: level, 34 | colorize: 'true', 35 | label: 'frame-emitter' 36 | } 37 | }); 38 | 39 | winston.loggers.add('uart', { 40 | console: { 41 | level: level, 42 | colorize: 'true', 43 | label: 'hsu' 44 | } 45 | }); 46 | 47 | winston.loggers.add('i2c', { 48 | console: { 49 | level: level, 50 | colorize: 'true', 51 | label: 'hsu' 52 | } 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /src/pn532.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var util = require('util'); 3 | var EventEmitter = require('events').EventEmitter; 4 | 5 | var setupLogging = require('./logs'); 6 | setupLogging(process.env.PN532_LOGGING); 7 | var logger = require('winston').loggers.get('pn532'); 8 | 9 | var FrameEmitter = require('./frame_emitter').FrameEmitter; 10 | var frame = require('./frame'); 11 | var DataFrame = frame.DataFrame; 12 | var AckFrame = frame.AckFrame; 13 | var c = require('./constants'); 14 | 15 | class PN532 extends EventEmitter { 16 | /* 17 | @constructor 18 | @param {object} hal - An instance of node-serialport's SerialPort or node-i2c's i2c 19 | */ 20 | constructor(hal, options) { 21 | super(); 22 | options = options || {}; 23 | this.pollInterval = options.pollInterval || 1000; 24 | 25 | if (hal.constructor.name === 'SerialPort') { 26 | var PN532_UART = require('./pn532_uart'); 27 | this.hal = new PN532_UART(hal); 28 | } else if (hal.constructor.name === 'i2c') { 29 | var PN532_I2C = require('./pn532_i2c'); 30 | this.hal = new PN532_I2C(hal); 31 | } else { 32 | throw new Error('Unknown hardware type: ', hal.constructor.name); 33 | } 34 | 35 | this.frameEmitter = new FrameEmitter(this.hal); 36 | this.hal.init().then(() => { 37 | this.configureSecureAccessModule().then(() => this.emit('ready')); 38 | }); 39 | 40 | this.on('newListener', (event) => { 41 | // TODO: Only poll once (for each event type) 42 | if (event === 'tag') { 43 | logger.debug('Polling for tag scans...'); 44 | var scanTag = () => { 45 | this.scanTag().then((tag) => { 46 | this.emit('tag', tag); 47 | setTimeout(() => scanTag(), this.pollInterval); 48 | }); 49 | }; 50 | scanTag(); 51 | } 52 | }); 53 | } 54 | 55 | sendCommand(commandBuffer) { 56 | return new Promise((resolve, reject) => { 57 | 58 | var removeListeners = () => { 59 | logger.debug('Removing listeners'); 60 | this.frameEmitter.removeListener('frame', onFrame); 61 | this.frameEmitter.removeListener('error', onError); 62 | }; 63 | 64 | // Wire up listening to wait for response (or error) from PN532 65 | var onFrame = (frame) => { 66 | logger.debug('Response received for sendCommand', util.inspect(frame)); 67 | // TODO: If no ACK after 15ms, resend? (page 40 of user guide, UART only)? 68 | 69 | if (frame instanceof AckFrame) { 70 | logger.info('Command Acknowledged', util.inspect(frame)); 71 | } else if (frame instanceof DataFrame) { 72 | logger.info('Command Response', util.inspect(frame)); 73 | removeListeners(); 74 | resolve(frame); 75 | } 76 | }; 77 | this.frameEmitter.on('frame', onFrame); 78 | 79 | var onError = (error) => { 80 | logger.error('Error received for sendCommand', error); 81 | removeListeners(); 82 | reject(error); 83 | }; 84 | this.frameEmitter.on('error', onError); 85 | 86 | // Send command to PN532 87 | var dataFrame = new DataFrame(commandBuffer); 88 | var buffer = dataFrame.toBuffer(); 89 | logger.debug('Sending buffer:', util.inspect(buffer)); 90 | this.hal.write(buffer); 91 | }); 92 | } 93 | 94 | configureSecureAccessModule() { 95 | logger.info('Configuring secure access module (SAM)...'); 96 | 97 | // TODO: Test IRQ triggered reads 98 | 99 | var timeout = 0x00; // 0x00-0xFF (12.75 seconds). Only valid for Virtual card mode (SAMCONFIGURATION_MODE_VIRTUAL_CARD) 100 | 101 | var commandBuffer = [ 102 | c.COMMAND_SAMCONFIGURATION, 103 | c.SAMCONFIGURATION_MODE_NORMAL, 104 | timeout, 105 | c.SAMCONFIGURATION_IRQ_ON // Use IRQ pin 106 | ]; 107 | return this.sendCommand(commandBuffer); 108 | } 109 | 110 | getFirmwareVersion() { 111 | logger.info('Getting firmware version...'); 112 | 113 | return this.sendCommand([c.COMMAND_GET_FIRMWARE_VERSION]) 114 | .then((frame) => { 115 | var body = frame.getDataBody(); 116 | return { 117 | IC: body[0], 118 | Ver: body[1], 119 | Rev: body[2], 120 | Support: body[3] 121 | }; 122 | }); 123 | } 124 | 125 | getGeneralStatus() { 126 | logger.info('Getting general status...'); 127 | 128 | return this.sendCommand([c.COMMAND_GET_GENERAL_STATUS]) 129 | .then((frame) => { 130 | var body = frame.getDataBody(); 131 | return body; 132 | }); 133 | } 134 | 135 | scanTag() { 136 | logger.info('Scanning tag...'); 137 | 138 | var maxNumberOfTargets = 0x01; 139 | var baudRate = c.CARD_ISO14443A; 140 | 141 | var commandBuffer = [ 142 | c.COMMAND_IN_LIST_PASSIVE_TARGET, 143 | maxNumberOfTargets, 144 | baudRate 145 | ]; 146 | 147 | return this.sendCommand(commandBuffer) 148 | .then((frame) => { 149 | var body = frame.getDataBody(); 150 | logger.debug('body', util.inspect(body)); 151 | 152 | var numberOfTags = body[0]; 153 | if (numberOfTags === 1) { 154 | var tagNumber = body[1]; 155 | var uidLength = body[5]; 156 | 157 | var uid = body.slice(6, 6 + uidLength) 158 | .toString('hex') 159 | .match(/.{1,2}/g) 160 | .join(':'); 161 | 162 | return { 163 | ATQA: body.slice(2, 4), // SENS_RES 164 | SAK: body[4], // SEL_RES 165 | uid: uid 166 | }; 167 | } 168 | }); 169 | } 170 | 171 | readBlock(options) { 172 | logger.info('Reading block...'); 173 | 174 | var options = options || {}; 175 | 176 | var tagNumber = options.tagNumber || 0x01; 177 | var blockAddress = options.blockAddress || 0x01; 178 | 179 | var commandBuffer = [ 180 | c.COMMAND_IN_DATA_EXCHANGE, 181 | tagNumber, 182 | c.MIFARE_COMMAND_READ, 183 | blockAddress, 184 | ]; 185 | 186 | return this.sendCommand(commandBuffer) 187 | .then((frame) => { 188 | var body = frame.getDataBody(); 189 | logger.debug('Frame data from block read:', util.inspect(body)); 190 | 191 | var status = body[0]; 192 | 193 | if (status === 0x13) { 194 | logger.warn('The data format does not match to the specification.'); 195 | } 196 | var block = body.slice(1, body.length - 1); // skip status byte and last byte (not part of memory) 197 | // var unknown = body[body.length]; 198 | 199 | return block; 200 | }); 201 | } 202 | 203 | readNdefData() { 204 | logger.info('Reading data...'); 205 | 206 | return this.readBlock({blockAddress: 0x04}) 207 | .then((block) => { 208 | logger.debug('block:', util.inspect(block)); 209 | 210 | // Find NDEF TLV (0x03) in block of data - See NFC Forum Type 2 Tag Operation Section 2.4 (TLV Blocks) 211 | var ndefValueOffset = null; 212 | var ndefLength = null; 213 | var blockOffset = 0; 214 | 215 | while (ndefValueOffset === null) { 216 | logger.debug('blockOffset:', blockOffset, 'block.length:', block.length); 217 | if (blockOffset >= block.length) { 218 | throw new Error('Unable to locate NDEF TLV (0x03) byte in block:', block) 219 | } 220 | 221 | var type = block[blockOffset]; // Type of TLV 222 | var length = block[blockOffset + 1]; // Length of TLV 223 | logger.debug('blockOffset', blockOffset); 224 | logger.debug('type', type, 'length', length); 225 | 226 | if (type === c.TAG_MEM_NDEF_TLV) { 227 | logger.debug('NDEF TLV found'); 228 | ndefLength = length; // Length proceeds NDEF_TLV type byte 229 | ndefValueOffset = blockOffset + 2; // Value (NDEF data) proceeds NDEV_TLV length byte 230 | logger.debug('ndefLength:', ndefLength); 231 | logger.debug('ndefValueOffset:', ndefValueOffset); 232 | } else { 233 | // Skip TLV (type byte, length byte, plus length of value) 234 | blockOffset = blockOffset + 2 + length; 235 | } 236 | } 237 | 238 | var ndefData = block.slice(ndefValueOffset, block.length); 239 | var additionalBlocks = Math.ceil((ndefValueOffset + ndefLength) / 16) - 1; 240 | logger.debug('Additional blocks needing to retrieve:', additionalBlocks); 241 | 242 | // Sequentially grab each additional 16-byte block (or 4x 4-byte pages) of data, chaining promises 243 | var self = this; 244 | var allDataPromise = (function retrieveBlock(blockNum) { 245 | if (blockNum <= additionalBlocks) { 246 | var blockAddress = 4 * (blockNum + 1); 247 | logger.debug('Retrieving block:', blockNum, 'at blockAddress:', blockAddress); 248 | return self.readBlock({blockAddress: blockAddress}) 249 | .then(function(block) { 250 | blockNum++; 251 | ndefData = Buffer.concat([ndefData, block]); 252 | return retrieveBlock(blockNum); 253 | }); 254 | } 255 | })(1); 256 | 257 | return allDataPromise.then(() => ndefData.slice(0, ndefLength)); 258 | }) 259 | .catch(function(error) { 260 | logger.error('ERROR:', error); 261 | }); 262 | } 263 | 264 | writeBlock(block, options) { 265 | logger.info('Writing block...'); 266 | 267 | var options = options || {}; 268 | 269 | var tagNumber = options.tagNumber || 0x01; 270 | var blockAddress = options.blockAddress || 0x01; 271 | 272 | var commandBuffer = [].concat([ 273 | c.COMMAND_IN_DATA_EXCHANGE, 274 | tagNumber, 275 | c.MIFARE_COMMAND_WRITE_4, 276 | blockAddress 277 | ], block); 278 | 279 | return this.sendCommand(commandBuffer) 280 | .then((frame) => { 281 | var body = frame.getDataBody(); 282 | logger.debug('Frame data from block write:', util.inspect(body)); 283 | 284 | var status = body[0]; 285 | 286 | if (status === 0x13) { 287 | logger.warn('The data format does not match to the specification.'); 288 | } 289 | var block = body.slice(1, body.length - 1); // skip status byte and last byte (not part of memory) 290 | // var unknown = body[body.length]; 291 | 292 | return block; 293 | }); 294 | } 295 | 296 | writeNdefData(data) { 297 | logger.info('Writing data...'); 298 | 299 | // Prepend data with NDEF type and length (TLV) and append terminator TLV 300 | var block = [].concat([ 301 | c.TAG_MEM_NDEF_TLV, 302 | data.length 303 | ], data, [ 304 | c.TAG_MEM_TERMINATOR_TLV 305 | ]); 306 | 307 | logger.debug('block:', util.inspect(new Buffer(block))); 308 | 309 | var PAGE_SIZE = 4; 310 | var totalBlocks = Math.ceil(block.length / PAGE_SIZE); 311 | 312 | // Sequentially write each additional 4-byte pages of data, chaining promises 313 | var self = this; 314 | var allPromises = (function writeBlock(blockNum) { 315 | if (blockNum < totalBlocks) { 316 | var blockAddress = 0x04 + blockNum; 317 | var pageData = block.splice(0, PAGE_SIZE); 318 | 319 | if (pageData.length < PAGE_SIZE) { 320 | pageData.length = PAGE_SIZE; // Setting length will make sure NULL TLV (0x00) are written at the end of the page 321 | } 322 | 323 | logger.debug('Writing block:', blockNum, 'at blockAddress:', blockAddress); 324 | logger.debug('pageData:', util.inspect(new Buffer(pageData))); 325 | return self.writeBlock(pageData, {blockAddress: blockAddress}) 326 | .then(function(block) { 327 | blockNum++; 328 | // ndefData = Buffer.concat([ndefData, block]); 329 | return writeBlock(blockNum); 330 | }); 331 | } 332 | })(0); 333 | 334 | // return allDataPromise.then(() => ndefData.slice(0, ndefLength)); 335 | return allPromises; 336 | } 337 | 338 | // WIP 339 | authenticateBlock(uid, options) { 340 | logger.info('Authenticating block...'); 341 | 342 | var options = options || {}; 343 | 344 | var blockAddress = options.blockAddress || 0x04; 345 | var authType = options.authType || c.MIFARE_COMMAND_AUTH_A 346 | var authKey = options.authKey || [0xff, 0xff, 0xff, 0xff, 0xff, 0xff]; 347 | var tagNumber = options.tagNumber || 0x01; 348 | var uidArray = uid.split(':').map(s => Number('0x' + s)); 349 | 350 | var commandBuffer = [ 351 | c.COMMAND_IN_DATA_EXCHANGE, 352 | tagNumber, 353 | authType, 354 | blockAddress 355 | ].concat(authKey).concat(uidArray); 356 | 357 | return this.sendCommand(commandBuffer) 358 | .then((frame) => { 359 | var body = frame.getDataBody(); 360 | logger.info('Frame data from mifare classic authenticate', util.inspect(body)); 361 | 362 | console.log('body', body); 363 | return body; 364 | 365 | // var status = body[0]; 366 | // var tagData = body.slice(1, body.length); 367 | 368 | // return { 369 | // status: status.toString(16), 370 | // tagData: tagData 371 | // }; 372 | }); 373 | } 374 | } 375 | 376 | exports.PN532 = PN532; 377 | exports.I2C_ADDRESS = c.I2C_ADDRESS; 378 | -------------------------------------------------------------------------------- /src/pn532_i2c.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var EventEmitter = require('events').EventEmitter; 3 | var logger = require('winston').loggers.get('i2c'); 4 | 5 | class PN532_I2C extends EventEmitter { 6 | constructor(wire) { 7 | super(); 8 | this.wire = wire; 9 | } 10 | 11 | init() { 12 | logger.debug('Initializing I2C...'); 13 | return new Promise((resolve, reject) => { 14 | this.wire.on('data', (data) => { 15 | this.emit('data', data); 16 | }); 17 | 18 | this.wire.on('error', (error) => { 19 | this.emit('error', error); 20 | }); 21 | 22 | resolve(); 23 | }); 24 | } 25 | 26 | write(buffer) { 27 | this.wire.write(buffer); 28 | } 29 | } 30 | 31 | module.exports = PN532_I2C; 32 | -------------------------------------------------------------------------------- /src/pn532_uart.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var EventEmitter = require('events').EventEmitter; 3 | var util = require('util'); 4 | var logger = require('winston').loggers.get('uart'); 5 | 6 | class PN532_UART extends EventEmitter { 7 | constructor(serialPort) { 8 | super(); 9 | this.serial = serialPort; 10 | this.isAwake = false; 11 | } 12 | 13 | init() { 14 | logger.debug('Initializing serial port...'); 15 | return new Promise((resolve, reject) => { 16 | this.serial.on('open', (error) => { 17 | if (error) { 18 | logger.error('Error opening serial port:', util.inspect(error)); 19 | reject(); 20 | } 21 | 22 | this.serial.on('data', (data) => { 23 | this.emit('data', data); 24 | }); 25 | 26 | logger.debug('Serial port initialized.'); 27 | resolve(); 28 | }); 29 | this.serial.on('error', (error) => { 30 | logger.error('An error occurred on serial port:', util.inspect(error)); 31 | reject(); 32 | }); 33 | }); 34 | } 35 | 36 | write(buffer) { 37 | if (!this.isAwake) { 38 | logger.debug('Waking up PN532...'); 39 | var wakeup = new Buffer([0x55, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); 40 | buffer = Buffer.concat([wakeup, buffer]); 41 | this.isAwake = true; 42 | } 43 | 44 | this.serial.write(buffer); 45 | } 46 | } 47 | 48 | module.exports = PN532_UART; 49 | --------------------------------------------------------------------------------