How to integrate Node.js with UHF RFID reader?

December 12, 2021

Intro

Two years ago, I was working on a project to automate library operations. One of the main components of the system, namely, borrowing and returning books, relied on uniquely identifying each book by reading its unique RFID tags glued on its cover. Of course, since we were doing automation, it was meant to be read by a machine/computer, not by librarian’s eyes.

The enterprise that we were developing the project for, decided to use a UHF (Ultra High Frequency) RFID reader, the model is called UHF Long Range Reader - DL920, that can read tags in distance of up to 15 meters, pretty powerful, and all looking good!

Moreover, we had a Vue.js frontend made for the librarian to keep track of all the books in the library, manage borrows and returns. Our task was to somehow get the tag data out of the UHF reader to the browser.

Solution

No surprises, I was responsible for the non-frontend part of this task, i.e delivering tag identifier that is read by the UHF reader to the frontend application. I had no idea about UHF readers, and even worse, Google could not find me any proper resources to start with, nor npm could offer an SDK or a driver. 😟

The only thing I knew was that it is connected to a computer through an Ethernet(LAN) port and had a chunky 100-page manual. I digged into this book and extracted some useful knowledge and fundamentals after spending quite a long time reading it.

Let’s dig into them again together! đŸ“šđŸ€“

Two of the important concepts we need to get familiar are the command and response data structures. Basically, they represent the general contract of the hexadecimal data we send and receive from the reader.

Command data structure

Command data structure

  • Len: command data length (excluding itself), value equals length(Data[]) + 4
  • Adr: reader address, value range: 0-254; for broadcasting the value 255 is used, in that case, all connected readers will receive this command. We use 0 (or 0x00 in hex) for the current reader.
  • Cmd: operation command symbol.
  • Data[]: operation command parameters. We omit this, as we won’t be sending any parameters, hence Len=4.
  • LSB-CRC16: CRC-16 checksum, 2 bytes with least significant byte first.
  • MSB-CRC16: CRC-16 checksum, 2 bytes with most significant byte first.

See this section to see how to calculate CRC16 checksum.

Response data structure

Response data structure

  • Len: response data length (excluding itself), value equals length(Data[]) + 5
  • Adr: reader address, value range: 0-254.
  • reCmd: response command symbol. If the command was unrecognized, this value is 0x00.
  • Status: operation command symbol.
  • Data[]: operation command parameters. If Len=5, this will not exist.
  • LSB-CRC16: CRC-16 checksum, 2 bytes with least significant byte first.
  • MSB-CRC16: CRC-16 checksum, 2 bytes with most significant byte first.
What is CRC?

CRC (Cyclic Redundancy Check) is an error-detecting code commonly used in networks. Briefly speaking, we use them to check for the possibility of data corruption when it is transferred between network devices.

The manual provides a sample function to calculate/generate this code using the C language. Here is the Node.js/Javascript version I implemented:

const PRESET_VALUE = 0xFFFF;
const POLYNOMIAL = 0x8408;

const calculateCRC16bit = (pucY) => {
  let uiCrcValue = PRESET_VALUE;
  for (let i = 0; i < pucY.length; i++) {
    uiCrcValue ^= (pucY[i]);
    for (let j = 0; j < 8; j++) {
      uiCrcValue = (uiCrcValue >> 1) ^ ((uiCrcValue & 0x0001) && POLYNOMIAL)
    }
  }
  const buf = Buffer.from(uiCrcValue.toString(16), 'hex');
  const [msb, lsb] = buf;
  return Buffer.from([lsb, msb], 'hex');
};

Work modes

The reader we used has 2 work modes:

  • Active mode — the reader constantly listens for tags, not very useful for our purposes.

Command: [0x0a, 0x00, 0x35, 0x01, 0x02, 0x01, 0x00, 0x01, 0x00, 0x01, 0x9b]

  • Answer mode — listens for tags only given the command. We will be using this mode as we don’t want the reader start listening when it is powered on. We want it listen only when the librarian wants, and stop listening when the librarian doesn’t want.

Command: [0x0a, 0x00, 0x35, 0x00, 0x02, 0x01, 0x00, 0x01, 0x00, 0x2a, 0x9f]

Constructing the Inventory command

Finally, we will be issuing the Inventory command in Answer mode. Let’s construct this command using the data structure above.

  • Len: as we don’t need any parameters(length(Data[])=0), this value equals to 4, or 0x04 in hex.
  • Adr: use address 0x00, since we are using the default connection.
  • Cmd: we use the table provided in the manual, where Inventory corresponds to 0x01. Other useful commands may be, Read Data 0x02, Write data 0x03, etc.
  • LSB-CRC16, MSB-CRC16: using the function we defined above, we calculate the CRC16 for the command we have so far: [0x04, 0x00, 0x01]. Passing it to the function: calculateCRC16bit([0x04, 0x00, 0x01]) = [0xdb, 0x4b]

Hence, our final Inventory command becomes [0x04, 0x00, 0x01, 0xdb, 0x4b]

Node.js service

Now we have all the UHF reader-specific knowledge we need, we can start building the system in question.

It is always helpful to have the high-level architecture ready beforehand: Node.js UHF Architecture

In this diagram, you can see that the Node.js service is the core component of this system. It has the following responsibilities:

  • Talk to the Vue.js frontend using socket.io, and when is told to do so, it delivers the RFID tags received from the reader.
  • Talk to the RFID reader using TCP Sockets(we will see this in a bit) to send the commands that reader understands and receive the tag values in the visibility radius (this is configurable).
  • Serve as a “backend” to the Electron.js desktop app engine. I built a simple React based UI on Electron to easily turn on/off the reader, specify the IP and port of the reader to connect to.

TCP Sockets

Since the reader is a very-low level network device, we have to use the raw TCP socket client to establish communication with it, and transmit packets.

Node.js provides a powerful client out-of-the box, as part of the net module — the Socket class, which is also an Event emitter.

We may initialize it as follows:

import { Socket } from 'net';

const READER_PORT = 6000; // this is default port of the reader
const READER_IP = "192.168.1.190"; // the default IP

const reader = new Socket();

reader.setEncoding('ascii');
reader.connect(READER_PORT, READER_IP, () => {})
reader.on('connect', () => {
  // Successfully connected!
  // We are ready to send commands and receive data here  
}

We have decided to use the Answer-mode of the reader, it is the best time to tell the reader to switch to this work mode:

import { Socket } from 'net';

const ANSWER_MODE = Buffer.from([0x0a, 0x00, 0x35, 0x00, 0x02, 0x01, 0x00, 0x01, 0x00, 0x2a, 0x9f], 'hex');
const READER_PORT = 6000; // this is default port of the reader
const READER_IP = "192.168.1.190"; // the default IP

const reader = new Socket();

reader.setEncoding('ascii');
reader.connect(READER_PORT, READER_IP, () => {})
reader.on('connect', () => {
  // Successfully connected!
  // We are ready to issue commands and receive data here

  // Switch the reader to the ANSWER_MODE
  reader.write(ANSWER_MODE);
  // We don't receive the data yet!!!
  reader.on('data', data => {
    const buf = Buffer.from(data, 'ascii');
    const response = buf.toString('hex', 0, buf.length);
    console.log('RESPONSE', response);
  });
});

Reader does not return the tag values just yet. Since we switched it to Answer-mode, we need to tell it when to give them to us. In other words, we need to issue the Inventory command we constructed above, whenever we need the tag values.

Let’s ask him provide some tag values of the books around for us:

import { Socket } from 'net';

const ANSWER_MODE = Buffer.from([0x0a, 0x00, 0x35, 0x00, 0x02, 0x01, 0x00, 0x01, 0x00, 0x2a, 0x9f], 'hex');
const INVENTORY = Buffer.from([0x04, 0x00, 0x01, 0xdb, 0x4b], 'hex');
const READER_PORT = 6000; // this is default port of the reader
const READER_IP = "192.168.1.190"; // the default IP

const reader = new Socket();

reader.setEncoding('ascii');
reader.connect(READER_PORT, READER_IP, () => {})
reader.on('connect', () => {
  // Successfully connected!
  // We are ready to issue commands and receive data here

  // Switch the reader to the ANSWER_MODE
  reader.write(ANSWER_MODE);
  
  // Ask for tag values in the visibility radius
  reader.write(INVENTORY);
  // We start receiving data here  reader.on('data', data => {    const buf = Buffer.from(data, 'ascii');    const response = buf.toString('hex', 0, buf.length);    console.log('RESPONSE', response);  });});

We get something like RESPONSE 13000101010c620024315519000125605f352f24. Looking at our response data structure, one can interpret the portion that starts with "62", namely "620024315519000125605f35" is the string value for our tag.

So far so good! đŸ’ȘđŸ»

However, with this you get the values once only, when you bring in new books to the visibility radius, the reader does not react to them. Why? Answer is simple, because we haven’t told it to give the values again. So we better ask him to periodically look for tags, in some time intervals. For our purposes, we can set this interval to something around 100 ms:

import { Socket } from 'net';

const ANSWER_MODE = Buffer.from([0x0a, 0x00, 0x35, 0x00, 0x02, 0x01, 0x00, 0x01, 0x00, 0x2a, 0x9f], 'hex');
const INVENTORY = Buffer.from([0x04, 0x00, 0x01, 0xdb, 0x4b], 'hex');

const READER_PORT = 6000; // this is default port of the reader
const READER_IP = "192.168.1.190"; // the default IP

// Used to track the interval for inventory commandlet interval;
const reader = new Socket();
reader.setEncoding('ascii');
reader.connect(READER_PORT, READER_IP, () => {})
reader.on('connect', () => {
  // Successfully connected!
  // We are ready to issue commands and receive data here

  // Switch the reader to the ANSWER_MODE
  reader.write(ANSWER_MODE);
  
  // Ask for tag values in the visibility radius  interval = setInterval(() => {    reader.write(INVENTORY);  }, 100);
  // We start receiving data here
  reader.on('data', data => {
    const buf = Buffer.from(data, 'ascii');
    const response = buf.toString('hex', 0, buf.length);
    console.log('RESPONSE', response);
  });
});

Now, it behaves pretty much the way we want. We are nearly there! 🚀

Handling idle connection drops

If you don’t use the reader for few minutes, i.e don’t bring in any books to the radius, you’ll get the following error:

read ECONNRESET
    at TCP.onStreamRead (internal/stream_base_commons.js:208:20) {
  errno: 'ECONNRESET',
  code: 'ECONNRESET',
  syscall: 'read'
}

That is because, there has been no data flow through our connection(it’s been idle), and thus the connection dropped. Bear in mind that we established the low level TCP connection with net.Socket, and we are responsible for configuring it properly. Unlike some high level libraries, where everything works as you would imagine with 0 configuration, this is not the case with low level ones.

TCP protocol has something called the keep-alive functionality. This is a smart mechanism where the client sends an empty ACK packet, if there hasn’t been any packet sent during the specified keepalive interval, which is configurable, and if the other party responds with an ACK, then the client keeps the connection open.

net.Socket provides a handy function: socket.setKeepAlive([enable][, initialDelay]) to manipulate this parameter.

For our needs, we set enable=true and initialDelay=60000 (1 minute):

import { Socket } from 'net';

const ANSWER_MODE = Buffer.from([0x0a, 0x00, 0x35, 0x00, 0x02, 0x01, 0x00, 0x01, 0x00, 0x2a, 0x9f], 'hex');
const INVENTORY = Buffer.from([0x04, 0x00, 0x01, 0xdb, 0x4b], 'hex');

const READER_PORT = 6000; // this is default port of the reader
const READER_IP = "192.168.1.190"; // the default IP

// Used to track the interval for inventory command
let interval;

const reader = new Socket();
reader.setEncoding('ascii');
reader.setKeepAlive(true, 60000);
reader.connect(READER_PORT, READER_IP, () => {})
reader.on('connect', () => {
  // Successfully connected!
  // We are ready to issue commands and receive data here

  // Switch the reader to the ANSWER_MODE
  reader.write(ANSWER_MODE);
  
  // Ask for tag values in the visibility radius
  interval = setInterval(() => {
    reader.write(INVENTORY);
  }, 100);

  // We start receiving data here
  reader.on('data', data => {
    const buf = Buffer.from(data, 'ascii');
    const response = buf.toString('hex', 0, buf.length);
    console.log('RESPONSE', response);
  });
});

From now on, our connection stays open, even if the reader is not used for some time, and respond again when needed.

Remote control via Socket.io

The only thing left for us is to make it manageable from the Vue.js (or any *.js framework you want).

As it is shown it the diagram above, we use Socket.io to handle this:

  1. The client emits an event, let’s say we call it "startListening".
  2. We listen to this socket.on("startListening", () => {}), and when we get such an event, kick off our interval for Inventory.
  3. Use socket.emit('rfid', { value: response }); to deliver the parsed tag value to the frontend.
  4. When the client (the frontend) disconnects the socket, we stop our interval with clearInterval(), hence stop sending the Inventory commands.

I am not going to fully write it here, since it is quite simple and the rest of the logic can be implemented as per the business requirements.

Pheew! It has been a long one, hope you’ve found it useful!

See you later,
Cheers, Azamat!


abdullaev.dev
Profile picture

Engineering blog by Azamat Abdullaev.

I write my <discoveries />.

All opinions are my own.