@eeemarv/io-spi
Version:
Promise Based Native Node.js Addon for SPI devices on Linux
671 lines (602 loc) • 16 kB
JavaScript
// @ts-check
;
/**
* MFRC522 SPI test
* Read the version number and Perform self test
* Read 4, 7 and 10 byte UIDs from Mifare tags
*
* To run:
* node examples/mfrc522.js
*
* Optional flags:
*
* --speed : The default max speed of the SPI clock
* is 10Mhz.
* This can be changed with with--speed=<number>
* E.g. --speed=1_000_000 gives 1Mhz max clock speed
*
* --device The default device is /dev/spidev0.0
* Select another e.g --device=/dev/spidev0.1
*
* --no-self-test : To disable the self test. If
* your MFRC522 is a clone the self test may fail.
* In that case it is useful to disable the self test.
*
* Based on https://github.com/miguelbalboa/rfid
* And https://github.com/firsttris/mfrc522-rpi
* The NXP MFRC522 data sheet:
* https://www.nxp.com/docs/en/data-sheet/MFRC522.pdf
*/
import SPIDevice from '../index.mjs';
import { setTimeout } from 'timers/promises';
import { PCD_Cmd } from './mfrc522_data/pcd_command.js';
import { PCD_Reg } from './mfrc522_data/pcd_reg.js';
import { PICC_Cmd } from './mfrc522_data/picc_command.js';
import { PCD_Test } from './mfrc522_data/pcd_self_test.js';
let spi;
/**
* @param {number} test
* @returns {void}
*/
const testByte = (test) => {
if (!Number.isInteger(test)){
throw new TypeError(`Error, not an integer ${JSON.stringify(test)}`);
}
if (test > 255){
throw new RangeError(`Error out of range: ${JSON.stringify(test)}`);
}
if (test < 0){
throw new RangeError(`Error out of range: ${JSON.stringify(test)}`);
}
};
/**
* @param {number} addr
* @returns {number}
*/
const getReadAddr = (addr) => {
return ((addr << 1) & 0x7e) | 0x80;
};
/**
* @param {number} addr
* @returns {number}
*/
const getWriteAddr = (addr) => {
return (addr << 1) & 0x7e;
};
/**
* Read from the MFRC522
* @param {number[]} addrAry registers to read
* @returns {Promise<number[]>} values of the registers
*/
const read = async (addrAry) => {
if (!Array.isArray(addrAry)){
throw new TypeError(`addrAry is not an array: ${JSON.stringify(addrAry)}`);
}
if (!addrAry.length){
throw new RangeError('read addrAry is empty');
}
for (const addr of addrAry){
testByte(addr);
}
const [rxBuf] = await spi.transfer([
Buffer.from([
...addrAry.map((addr) => getReadAddr(addr)),
0x00
])
]);
return [...rxBuf].slice(1); // Skip 1 byte
};
/**
* Write to the MFRC522
* The first element in each nested array is
* the register address to write to,
* the following byte(s) the data
* @param {number[][]} writeAry
* @returns {Promise<void>}
*/
const write = async (writeAry) => {
if (!Array.isArray(writeAry)){
throw new TypeError(`writeAry is not an array: ${JSON.stringify(writeAry)}`);
}
if (!writeAry.length){
throw new RangeError('writeAry is empty');
}
for (const subAry of writeAry){
if (!Array.isArray(subAry)){
throw new TypeError(`write subAry is not an array: ${JSON.stringify(subAry)}`)
}
if (!subAry.length){
throw new RangeError('write subAry is empty');
}
if (subAry.length < 2){
throw new RangeError(`write subAry needs a least two elements, an address and register: ${JSON.stringify(subAry)}`);
}
for (const test of subAry){
testByte(test);
}
}
const lastWr = writeAry.pop();
await spi.transfer([
...writeAry.map((wr) => { return {
tx_buf: Buffer.from([getWriteAddr(wr[0]), ...wr.slice(1)]),
cs_change: 1
}}),
// @ts-ignore
Buffer.from([getWriteAddr(lastWr[0]), ...lastWr.slice(1)])
]);
};
/**
* Set bits in a register of the MFRC522
* @param {number} addr
* @param {number} bitMask
* @returns {Promise<void>}
*/
const setBitMask = async (addr, bitMask) => {
if (!bitMask){
return;
}
const [value] = await read([addr]);
const newValue = value | bitMask;
if (value == newValue){
return;
}
await write([[addr, newValue]]);
};
/**
* Clear bits in a register of the MFRC522
* @param {number} addr
* @param {number} bitMask
* @returns {Promise<void>}
*/
const clearBitMask = async (addr, bitMask) => {
if (!bitMask){
return;
}
const [value] = await read([addr]);
const newValue = value & ~bitMask;
if (value == newValue){
return;
}
await write([[addr, newValue]]);
};
/**
* Soft Reset the MFRC522
* @returns {Promise<void>}
*/
const reset = async () => {
await write([[PCD_Reg.Command, PCD_Cmd.SoftReset]]);
await setTimeout(50);
};
/**
* Run the Self test (for digital integrety) of the MFRC522
* @returns {Promise<void>}
*/
const selfTest = async () => {
const [version] = await read([PCD_Reg.Version]);
console.log(`MFRC522 Version: \x1b[1;33m0x${version.toString(16)}\x1b[0m`);
if (version !== 0x92 && version !== 0x91 && version !== 0x90) {
throw new Error('Failed to initialize MFRC522 - wrong version');
}
await write([[PCD_Reg.Command, PCD_Cmd.SoftReset]]);
await setTimeout(50);
await write([
[PCD_Reg.Command, PCD_Cmd.Idle],
// flush FIFO
[PCD_Reg.FIFOLevel, 0x80],
// write 25x 0x00 to FIFO
[PCD_Reg.FIFOData, ...new Array(25).fill(0x00)],
// Copy 0x00's to internal memory
[PCD_Reg.Command, PCD_Cmd.Mem],
// stop
[PCD_Reg.Command, PCD_Cmd.Idle],
// flush FIFO
[PCD_Reg.FIFOLevel, 0x80],
// enable self test
[PCD_Reg.AutoTest, 0x09],
// write 0x00 to FIFO
[PCD_Reg.FIFOData, 0x00],
// Calc CRC
[PCD_Reg.Command, PCD_Cmd.CalcCRC],
]);
// Wait for CRCIRq
for(let i = 0; i < 100; i++) {
await setTimeout(1);
const [irq] = await read([PCD_Reg.DivIrq]); // DivIrqReg
if (irq & 0x04) break;
}
// Read FIFO to get test result
const result = await read(new Array(64).fill(PCD_Reg.FIFOData));
console.log('Self test data:');
for (let a = 0; a < 8; a++){
let str = '\x1b[32m';
for (let b = 0; b < 8; b++){
str += result[(a * 8) + b].toString(16).padStart(2, '0');
str += ' ';
}
console.log(str + '\x1b[0m');
}
await write([[PCD_Reg.AutoTest, 0x00]]);
// Compare with the expected result
for (const prop in PCD_Test){
if (version != PCD_Test[prop].version){
continue;
}
const expected = PCD_Test[prop].data;
const ok = expected.every((val, i) => val === result[i]);
if (ok){
console.log('Expected data matches for Self test.');
} else {
throw new Error('MFRC522 self-test failed: output does not match reference pattern.');
}
}
// Check if RF can be turned on
await setBitMask(PCD_Reg.TxControl, 0x03); // enable antenna drivers
const [txControl] = await read([PCD_Reg.TxControl]);
if ((txControl & 0x03) == 0x03) {
console.log('Able to turn on antenna.');
} else {
throw new Error('Failed to enable antenna (TxControlReg)');
}
console.log('MFRC522 Self test completed.');
};
/**
* Init the MFRC522 for transmission
* @returns {Promise<void>}
*/
const initRegs = async () => {
// antenna on
await setBitMask(PCD_Reg.TxControl, 0x03);
await write([
// 106 kbit/s type A (default)
[PCD_Reg.TxMode, 0x00],
[PCD_Reg.RxMode, 0x00],
// reset modwidth
[PCD_Reg.ModWidth, 0x26],
// Timer: TAuto=1; timer starts automatically at transmission end
// TAuto=1, timer prescaler
[PCD_Reg.TMode, 0x8d],
// 40kHz
[PCD_Reg.TPrescaler, 0x3e],
// 25ms before timeout
[PCD_Reg.TReloadH, 0x00],
[PCD_Reg.TReloadL, 0x1e],
// force 100% ASK modulation
[PCD_Reg.TxASK, 0x40],
// preset 0x6363 fpr CRC
[PCD_Reg.Mode, 0x3d],
]);
};
/**
* Initialize communication with the MFRC522
* @return {Promise<void>}
*/
const init = async () => {
let speed = 10_000_000;
let device = '/dev/spidev0.0';
let selfTestEnabled = true;
const args = process.argv.slice(2);
args.forEach((arg, i) => {
if (arg === '--no-self-test'){
selfTestEnabled = false;
return;
}
if (arg.startsWith('--speed')) {
const speedStr = arg.includes('=') ? arg.split('=')[1] : args[i + 1];
if (speedStr){
speed = Number(speedStr.replace(/_/g, ''));
}
}
if (arg.startsWith('--device')) {
const deviceStr = arg.includes('=') ? arg.split('=')[1] : args[i + 1];
if (deviceStr){
device = deviceStr;
}
}
});
spi = new SPIDevice(device, {
max_speed_hz: speed
});
try {
console.log(`SPI device: \x1b[1;33m${device}\x1b[0m`);
console.log(`SPI max speed Hz: \x1b[1;33m${spi.getMaxSpeedHz()}\x1b[0m`);
console.log(`SPI Mode: \x1b[1;33m${spi.getMode()}\x1b[0m`);
console.log(`SPI bits per word: \x1b[1;33m${spi.getBitsPerWord()}\x1b[0m`);
if (selfTestEnabled){
await reset();
await selfTest();
}
await reset();
await initRegs();
console.log('\x1b[1;32mMFRC522 initialized successfully\x1b[0m');
} catch (err) {
console.error('\x1b[1;31mMFRC522 initialization failed\x1b[0m:', err);
throw err;
}
console.log('Ready to read UIDs from tags. Press Ctrl-C to exit.');
};
/**
* Communicate with a tag through the MFRC522
* @param {number[]} dataAry - sent to the card
* @returns {Promise<{success: boolean, data: number[], bitSize: number}>}
*/
const transeive = async (dataAry) => {
const [comIrq1, fifoLevel1, bitFraming1] = await read([
PCD_Reg.ComIrq,
PCD_Reg.FIFOLevel,
PCD_Reg.BitFraming,
]);
await write([
// interrupt request enabled
[PCD_Reg.ComIEn, 0xf7],
// clear all interupt requests
[PCD_Reg.ComIrq, comIrq1 & 0x7f],
// flush FIFO
[PCD_Reg.FIFOLevel, fifoLevel1 | 0x80],
// Stop calculating CRC for new data in the FIFO
[PCD_Reg.Command, PCD_Cmd.Idle],
// data to FIFO
[PCD_Reg.FIFOData, ...dataAry],
// Transeive
[PCD_Reg.Command, PCD_Cmd.Transceive],
// start send
[PCD_Reg.BitFraming, bitFraming1 | 0x80],
]);
//Wait for the received data to complete
let irq = 0;
let timeout = true;
for (let i = 0; i < 8; i++){
await setTimeout(3);
[irq] = await read([PCD_Reg.ComIrq]);
if (!(irq & 0x01)){
timeout = false;
break;
}
if (!(irq & 0x30)){ // WaitIRq
timeout = false;
break;
}
}
// start send = 0
await clearBitMask(PCD_Reg.BitFraming, 0x80);
if (timeout){
return {success: false, data: [], bitSize: 0};
}
const [
error,
fifoLevel,
control
] = await read([
PCD_Reg.Error,
PCD_Reg.FIFOLevel,
PCD_Reg.Control
]);
if (error & 0x1b){
return {success: false, data: [], bitSize: 0};
}
let success = true;
let bitSize;
if (irq & 0x01) {
success = false;
}
let byteSize = fifoLevel;
let lastBits = control & 0x07;
if (lastBits) {
bitSize = (byteSize - 1) * 8 + lastBits;
} else {
bitSize = byteSize * 8;
}
if (byteSize == 0) {
byteSize = 1;
}
if (byteSize > 16) {
byteSize = 16;
}
// Read data from FIFO
const data = await read(new Array(byteSize).fill(PCD_Reg.FIFOData));
return { success, data, bitSize };
};
/**
* Detect if a tag is present in the antenna field
* @returns {Promise<boolean>} card detected
*/
const detect = async () => {
await write([[PCD_Reg.BitFraming, 0x07]]);
const {success, data, bitSize} = await transeive([PICC_Cmd.REQA]);
if (!success){
return false;;
}
if (bitSize != 0x10) {
return false;
}
if (data.length != 2){
return false;
}
return true;
};
/**
* Calculate a CRC on the co-processor of the MFRC522
* @param {number[]} dataAry
* @returns {Promise<void|number[]>}
*/
const calcCRC = async (dataAry) => {
// Clear the CRCIRq interrupt request bit
await clearBitMask(PCD_Reg.DivIrq, 0x04);
// Flush FIFO
await setBitMask(PCD_Reg.FIFOLevel, 0x80);
// data to FIFO and execute CalcCRC
await write([
[PCD_Reg.FIFOData, ...dataAry],
[PCD_Reg.Command, PCD_Cmd.CalcCRC],
]);
for (let i = 0; i < 1000; i++){
const [irq] = await read([PCD_Reg.DivIrq]);
if (irq & 0x04){
return await read([
PCD_Reg.CRCResultL, PCD_Reg.CRCResultH,
]);
}
}
};
/**
* Select anticollision for cascade leveland
* get SAK (select acknowledge) from a tag
* @param {number} level
* @returns {Promise<void|number[]>}
*/
const cascade = async (level) => {
if (![1, 2, 3].includes(level)){
throw new RangeError(`level must be 1, 2 or 3, current: ${level}`);
}
const cmd = [
PICC_Cmd.SEL_CL1,
PICC_Cmd.SEL_CL2,
PICC_Cmd.SEL_CL3,
][level - 1];
const cas1 = [cmd, 0x20];
for (let i = 0; i < 5; i++){
if (!i){
await setTimeout(2);
}
await write([[PCD_Reg.BitFraming, 0x00]]);
const {success, data, bitSize} = await transeive(cas1);
if (!success){
continue;
}
if (bitSize != 40){
continue;
}
// Check BCC (data[4])
if ((data[0] ^ data[1] ^ data[2] ^ data[3]) != data[4]){
continue;
}
return data;
}
};
/**
* Return a 4, 7 or 10 byte UID
* from a tag in the antenna field
* @returns {Promise<void|number[]>}
*/
const getUid = async () => {
const uid1 = await cascade(1);
if (!Array.isArray(uid1)){
return;
}
const sak1Req = [
PICC_Cmd.SEL_CL1, 0x70, ...uid1
];
const crc1 = await calcCRC(sak1Req);
if (!Array.isArray(crc1)){
console.log('\x1b[1;35mCRC1 failed\x1b[0m');
return;
}
const sak1 = await transeive([...sak1Req, ...crc1]);
if (!sak1.success){
console.log('\x1b[1;35mSAK1 no success\x1b[0m');
return;
}
if (!(sak1.data[0] & 0x04)){
if (uid1[0] == 0x88){
// error, indicates uid is longer
return;
}
// UID 4 bytes
return [...uid1.slice(0, 4)];
}
if (uid1[0] != 0x88){
// error, not valid for next level
return;
}
const uid2 = await cascade(2);
if (!Array.isArray(uid2)){
return;
}
const sak2Req = [
PICC_Cmd.SEL_CL2, 0x70, ...uid2
];
const crc2 = await calcCRC(sak2Req);
if (!Array.isArray(crc2)){
console.log('\x1b[1;35mCRC2 failed\x1b[0m');
return;
}
const sak2 = await transeive([...sak2Req, ...crc2]);
if (!sak2.success){
console.log('\x1b[1;35mSAK2 no success\x1b[0m');
return;
}
if (!(sak2.data[0] & 0x04)){
if (uid2[0] == 0x88){
// error, indicates uid is longer
return;
}
// UID 7 bytes
return [...uid1.slice(1, 4), ...uid2.slice(0, 4)];
}
if (uid2[0] != 0x88){
// error, not valid for next level
return;
}
const uid3 = await cascade(3);
if (!Array.isArray(uid3)){
return;
}
const sak3Req = [
PICC_Cmd.SEL_CL3, 0x70, ...uid3
];
const crc3 = await calcCRC(sak3Req);
if (!Array.isArray(crc3)){
console.log('\x1b[1;35mCRC3 failed\x1b[0m');
return;
}
const sak3 = await transeive([...sak3Req, ...crc3]);
if (!sak3.success){
console.log('\x1b[1;35mSAK3 no success\x1b[0m');
return;
}
// UID 10 bytes
return [...uid1.slice(1, 4), ...uid2.slice(1, 4), ...uid3.slice(0, 4)];
};
/**
* Main program
*/
(async () => {
let busy = false;
let readCount = 0;
let errorCount = 0;
await init();
// scan loop
setInterval(async () => {
if (busy){
console.log('\x1b[36m..reader busy, skip loop\x1b[0m');
return;
}
busy = true;
try {
await write([[PCD_Reg.Command, PCD_Cmd.SoftReset]]);
await initRegs();
const detected = await detect();
if (!detected){
return;
}
const uid = await getUid();
if (Array.isArray(uid)){
let uidStr = '';
for(const b of uid){
uidStr += b.toString(16).padStart(2, '0');
}
readCount++;
console.log(`Tag UID: \x1b[1;32m${uidStr}\x1b[0m, read count: \x1b[1;33m${readCount}\x1b[0m`);
} else {
errorCount++;
console.log(`Error count \x1b[1;31m${errorCount}\x1b[0m`);
}
} catch (e) {
console.error(e);
throw e;
} finally {
// antenna off
await clearBitMask(PCD_Reg.TxControl, 0x03);
busy = false;
}
}, 50);
})();