Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add polling and NTAG support #8

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Due to lack of time and testing material, the binding is not complete :
* Mifare Ultralight : fully Supported
* Mifare Classic 1K/4K : Partially supported (enough to authenticate, write and read a data block)
* Mifare DESFire : Partially supported (enough to select an application, authenticate in DES/3DES and read/write on a file)
* NTAG21x : Partially supported (read/write, authnetication)

If you need Freefare function which are currently not bound, submit an issue, a pull request or contact me by email.

Expand Down Expand Up @@ -66,6 +67,12 @@ List of detected tags

**Returns**: `Promise.<Array.<(Tag|MifareUltralightTag|MifareClassicTag|MifareDesfireTag)>>`, A promise to the list of `Tag`

#### Device.poll()

Polls for NFC devices infinitely. When a device is found it returns the tag.

**Returns**: `Promise.<Array.<(Tag|MifareUltralightTag|MifareClassicTag|MifareDesfireTag|Ntag)>>`, A promise of a `Tag`

#### Device.abort()

Abort command blocking the device like open().
Expand Down
2 changes: 1 addition & 1 deletion binding.gyp
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{
"target_name": "freefare",
"defines": [ 'V8_DEPRECATION_WARNINGS=1', '_FILE_OFFSET_BITS=32' ],
"sources": [ "src/addon.cpp", "src/freefare.cpp", "src/device.cpp", "src/tag.cpp", "src/tag_mifareultralight.cpp", "src/tag_mifareclassic.cpp", "src/tag_mifaredesfire.cpp" ],
"sources": [ "src/addon.cpp", "src/freefare.cpp", "src/device.cpp", "src/tag.cpp", "src/tag_mifareultralight.cpp", "src/tag_mifareclassic.cpp", "src/tag_mifaredesfire.cpp", "src/tag_ntag.cpp" ],
"include_dirs" : [
"<!(node -e \"require('nan')\")",
"/usr/include"
Expand Down
104 changes: 104 additions & 0 deletions examples/ntag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// This example is used with the NTAG216, or the NTAG21x devices without authentication.
// On the first detection of a card it will attempt a read and if it suceeds, write some data
// and enable authentication. If the read fails it assumes authentication is enabled and
// attempts and authentication. On success it reads some data and disabled authentication.
'use strict';

const Freefare = require('../index');

// Initialise Library
const freefare = new Freefare();

process.on('unhandledRejection', function(error, p) {
console.log('Unhandled Rejection at: Promise', p, 'reason:', error);
});

// The 4 byte buffer for the tag password.
const kPassword = Buffer.from([0x11, 0x22, 0x33, 0x44]);
// The 2 pack bytes. These must be the same when setting auth and authenticating.
// Consider them as an extra 2 bytes of password but not as secure.
const kPack = Buffer.from([0xaa, 0xaa]);

async function tagAdded(device, tag) {
if (tag.getType() == 'NTAG_21x') {
await tag.open();
await tag.getInfo();
const subtype = await tag.getSubType();
console.log(`${tag.getFriendlyName()} - ${tag.getUID()} - ${subtype}`);
try {
await tag.write(0x27, Buffer.from([0x01, 0x02, 0x03, 0x04]));
console.log('Read data from 0x27', await tag.read(0x27));
// If the read suceeds then do set auth.
try {
await tag.setAuth(kPassword, kPack, 0x00, true);
console.log('Tag auth set');
} catch (err) {
console.log('Unable to set tag auth', err);
}
} catch (err) {
console.log('Unable to read from tag, attempting auth', err);
try {
// If other commands are run before this then the tag must be re-opened.
await tag.close();
await tag.open();
await tag.authenticate(kPassword, kPack);
console.log('Tag authentication success, attempting read');
console.log('Read data from 0x27', await tag.read(0x27));

console.log('Disabling auth');
try {
await tag.disableAuth();
} catch (err) {
console.log('Failed to remove authentication');
}
} catch (err) {
console.log('Tag authenticate failed', err);
}
}

await tag.close();
} else {
console.log(`${tag.getFriendlyName()} - ${tag.getUID()}`);
}
startPoll(device);
}

let lastTag = null;
const kCardTimeout = 5 * 1000;
async function listTags(device) {
try {
const tag = await device.poll();
if (tag) {
if (!lastTag || Date.now() - lastTag.time > kCardTimeout || lastTag.uid != tag.getUID()) {
lastTag = {
time: Date.now(),
uid: tag.getUID()
};
try {
console.time('tagAdded');
await tagAdded(device, tag);
console.timeEnd('tagAdded');
} catch (err) {
console.log('tagAdded call failed', err);
startPoll(device);
}
} else {
startPoll(device);
}
}
} catch (err) {
console.log('Failed to list tags', err);
}
}

function startPoll(device) {
setTimeout(function() {
listTags(device);
}, 0);
}

(async function() {
const devices = await freefare.listDevices();
await devices[0].open();
startPoll(devices[0]);
})();
216 changes: 193 additions & 23 deletions lib/freefare.js
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -103,39 +103,64 @@ class Device {
});
}

parseTag(cppTag) {
if (!cppTag) {
return null;
}
switch (cppTag.getTagType()) {
case 'MIFARE_CLASSIC_1K':
case 'MIFARE_CLASSIC_4K':
return new MifareClassicTag(cppTag);
case 'MIFARE_DESFIRE':
return new MifareDesfireTag(cppTag);
case 'MIFARE_ULTRALIGHT':
return new MifareUltralightTag(cppTag);
case 'NTAG_21x':
return new NtagTag(cppTag);
case 'MIFARE_ULTRALIGHT_C':
default:
return new Tag(cppTag);
}
}

parseList(list) {
let res = [];
for (let cppTag of list) {
const tag = this.parseTag(cppTag);
if (tag) {
res.push(tag);
}
}
return res;
}

/**
* List of detected tags
* @return {Promise<Array<Tag|MifareUltralightTag|MifareClassicTag|MifareDesfireTag>>} A promise to the list of `Tag`
*/
listTags() {
return new Promise((resolve, reject) => {
this[cppObj].listTags((error, list) => {
if(error) {
switch (error) {
default:
reject(new Error('Unknown error (' + error + ')'));
}
if (error) {
return reject(new Error(error));
}

let res = [];
for (let cppTag of list) {
switch(cppTag.getTagType()) {
case 'MIFARE_CLASSIC_1K':
case 'MIFARE_CLASSIC_4K':
res.push(new MifareClassicTag(cppTag));
break;
case 'MIFARE_DESFIRE':
res.push(new MifareDesfireTag(cppTag));
break;
case 'MIFARE_ULTRALIGHT':
res.push(new MifareUltralightTag(cppTag));
break;
case 'MIFARE_ULTRALIGHT_C':
default:
res.push(new Tag(cppTag));
}
resolve(this.parseList(list));
});
});
}

/**
* Wait infinitely for a tag to be added.
* @return {Promise<Tag|MifareUltralightTag|MifareClassicTag|MifareDesfireTag>} A promise to of `Tag`
*/
poll() {
return new Promise((resolve, reject) => {
this[cppObj].poll((error, tag) => {
if (error) {
return reject(new Error(error));
}
resolve(res);
resolve(this.parseTag(tag));
});
});
}
Expand Down Expand Up @@ -699,7 +724,152 @@ class MifareDesfireTag extends Tag {
});
});
}
}

/**
* A NTAG_21x tag
*
* @class NTAG_21x Tag
* @extends Tag
*/
class NtagTag extends Tag {
constructor(cppTag) {
super(cppTag);
}

wrap(fn) {
const $arguments = Array.from(arguments).slice(1);
const $this = this;
return new Promise(function(resolve, reject) {
$arguments.push(function(error, data) {
if (error) {
reject(error);
} else {
resolve(data);
}
});
fn.apply($this[cppObj], $arguments);
});
}

/**
* Open tag for further communication
* @return {Promise} A promise to the end of the action.
*/
open() {
return this.wrap(this[cppObj].ntag_connect);
}

/**
* Close a tag
* @return {Promise} A promise to the end of the action.
*/
close() {
return this.wrap(this[cppObj].ntag_disconnect);
}

/**
* Retrieve info such as the sub-type.
* @return {Promise} A promise to the end of the action.
*/
getInfo() {
return this.wrap(this[cppObj].ntag_getInfo);
}

/**
* Gets the tag sub-type as a string. getInfo must be called first.
* @return {Promise} A promise to the end of the action which returns the sub-type.
*/
getSubType() {
return this.wrap(this[cppObj].ntag_getType);
}

/**
* Writes a 4 byte buffer to the given page.
* @param page The given page to write to. Must be between 0x00 and 0xff inclusive.
* @param buffer A 4 byte buffer to write to the given page.
* @return {Promise} A promise to the end of the action.
*/
write(page, buffer) {
if (page < 0 || page > 0xff) {
throw new Error('Invalid page');
}
if (!(buffer instanceof Buffer)) {
throw new Error('Data is not a buffer');
}
if (buffer.length != 4) {
throw new Error('Data is not 4 bytes');
}
return this.wrap(this[cppObj].ntag_write, page, buffer);
}

/**
* Reads a 4 byte buffer from the given page.
* @param page The given page to write to. Must be between 0x00 and 0xff inclusive.
* @return {Promise} A promise to the end of the action which returns a 4 byte buffer of the page.
*/
read(page) {
if (page < 0 || page > 0xff) {
throw new Error('Invalid page');
}
return this.wrap(this[cppObj].ntag_read, page);
}

/**
* Set a password for writing or reading and writing to tag.
* @param password A 4 byte buffer representing the password.
* @param pack A 2 byte buffer representing the password acknowledgement bytes.
* These could be considered as an extra 2 password bytes.
* @param startPage The page to start the protection from 0x00-0xff.
* @param prot Set to true to enable password protection for reads. False to only password protect writes.
* @return {Promise} A promise to the end of the action.
*/
setAuth(password, pack, startPage, prot) {
if (!(password instanceof Buffer)) {
throw new Error('Password is not a buffer');
}
if (password.length != 4) {
throw new Error('Password is not 4 bytes');
}
if (!(pack instanceof Buffer)) {
throw new Error('Pack is not a buffer');
}
if (pack.length != 2) {
throw new Error('Pack is not 2 bytes');
}
prot = prot ? true : false;
if (startPage < 0 || startPage > 0xff) {
throw new Error('Invalid start page');
}
return this.wrap(this[cppObj].ntag_set_auth, password, pack, startPage, prot);
}

disableAuth() {
return this.wrap(this[cppObj].ntag_disable_auth);
}

/**
* Authenticates a tag with a given password.
* @param password A 4 byte buffer representing the password.
* @param pack A 2 byte buffer representing the password acknowledgement bytes.
* These could be considered as an extra 2 password bytes.
* @return {Promise} A promise to the end of the action.
*/
authenticate(password, pack) {
if (!(password instanceof Buffer)) {
throw new Error('Password is not a buffer');
}
if (password.length != 4) {
throw new Error('Password is not 4 bytes');
}
if (!(pack instanceof Buffer)) {
throw new Error('Pack is not a buffer');
}
if (pack.length != 2) {
throw new Error('Pack is not 2 bytes');
}
return this.wrap(this[cppObj].ntag_authenticate, password, pack);
}
}

module.exports = Freefare;
Loading