NodeMCU and RC522 card reader

Some time ago an opportunity knocked on my door. A friend needs a device that would read a card, send the id to the server and react to response.
Today we will work on code and create a class and a handler for the RC522 module.

Module and handler added to NodeMCU boilerplate @ GitHub

Planning

What I need is to read card ID and send it to a server for a check. But before that, we need to write a module and find out how RC522 works πŸ™‚
But we are lucky! Someone already implemented RC522 in LUA. It is available at GitHub: https://github.com/capella-ben/LUA_RC522. This lib implements much more functions than id read, it can write and read to/from a card. What worries me is the size of it. It is huge. After cloning it I uploaded it to the NodeMCU and… memory error πŸ™‚ Just as I was afraid.
Slightly change in plan, let us remove all unnecessary functions and create a worker for the rc522 module.

Wiring

How can we live without circuit? We cannot! Let’s connect our module to NodeMCU and add a tri-led to the package

RFID-RC522      NodeMCUv2           tri-led

SDA  ---------- D4     D8 ---------- blue
SCK  ---------- D5     D3 ---------- red
MOSI ---------- D7     D1 ---------- green
MISO ---------- D6     3V ---------- anode
IRQ  
GND  ---------- GND
RST  ---------- D2
3.3V ---------- 3V

Until now, we have a startup breaker connected to D1, from now on we move it to D0. It stops booting when D0 is grounded. And there is no mistake, we are using NodeMCU v2 because it is smaller πŸ™‚

RC522 module

We have the code but we need to work on it. Let’s start with moving variables to class:

local RC522 = {}
RC522.__index = RC522
RC522.mode_idle = 0x00
RC522.mode_auth = 0x0E
RC522.mode_transrec = 0x0C
RC522.mode_reset = 0x0F
RC522.mode_crc = 0x03
RC522.act_anticl = 0x93
RC522.reg_tx_control = 0x14
RC522.length = 16
RC522.num_write = 0
RC522.pin_ss = nil
RC522.pin_rst = nil
RC522.rc_timer = tmr.create()

In the original code, the timer was registered in the old way so we need to upgrade the timer to OO.

function RC522.dev_write(address, value)
    gpio.write(RC522.pin_ss, gpio.LOW)
    RC522.num_write = spi.send(1, bit.band(bit.lshift(address,1), 0x7E), value)
    gpio.write(RC522.pin_ss, gpio.HIGH)
end
function RC522.appendHex(t)
  strT = ""
  for i,v in ipairs(t) do
    strT = strT.." "..string.format("%X", t[i])
  end
  return strT
end

function RC522.dev_read(address)
    local val = 0;
    gpio.write(RC522.pin_ss, gpio.LOW)
    spi.send(1,bit.bor(bit.band(bit.lshift(address,1), 0x7E), 0x80))
    val = spi.recv(1,1)
    gpio.write(RC522.pin_ss, gpio.HIGH)
    return string.byte(val)
end

function RC522.set_bitmask(address, mask)
    local current = RC522.dev_read(address)
    RC522.dev_write(address, bit.bor(current, mask))
end

function RC522.clear_bitmask(address, mask)
    local current = RC522.dev_read(address)
    RC522.dev_write(address, bit.band(current, bit.bnot(mask)))
end

function RC522.getFirmwareVersion()
  return RC522.dev_read(0x37)
end

function RC522.request()
    req_mode = { 0x26 }
    err = true
    back_bits = 0
    RC522.dev_write(0x0D, 0x07)
    err, back_data, back_bits = RC522.card_write(RC522.mode_transrec, req_mode)
    if err or (back_bits ~= 0x10) then
        return false, nil
     end
    return true, back_data
end

function RC522.card_write(command, data)
    back_data = {}
    back_length = 0
    local err = false
    local irq = 0x00
    local irq_wait = 0x00
    local last_bits = 0
    n = 0
    if command == RC522.mode_auth then
        irq = 0x12
        irq_wait = 0x10
    end   
    if command == RC522.mode_transrec then
        irq = 0x77
        irq_wait = 0x30
    end
    RC522.dev_write(0x02, bit.bor(irq, 0x80))
    RC522.clear_bitmask(0x04, 0x80) 
    RC522.set_bitmask(0x0A, 0x80) 
    RC522.dev_write(0x01, RC522.mode_idle)
    for i,v in ipairs(data) do
        RC522.dev_write(0x09, data[i])
    end
    RC522.dev_write(0x01, command)
    if command == RC522.mode_transrec then
        RC522.set_bitmask(0x0D, 0x80)
    end
    i = 25
    while true do
        tmr.delay(1)
        n = RC522.dev_read(0x04)
        i = i - 1
        if  not ((i ~= 0) and (bit.band(n, 0x01) == 0) and (bit.band(n, irq_wait) == 0)) then
            break
        end
    end    
    RC522.clear_bitmask(0x0D, 0x80)
    if (i ~= 0) then
        if bit.band(RC522.dev_read(0x06), 0x1B) == 0x00 then
            err = false            
            if (command == RC522.mode_transrec) then
                n = RC522.dev_read(0x0A)
                last_bits = bit.band(RC522.dev_read(0x0C),0x07)
                if last_bits ~= 0 then
                    back_length = (n - 1) * 8 + last_bits
                else
                    back_length = n * 8
                end
                if (n == 0) then
                    n = 1
                end 
                if (n > RC522.length) then
                    n = RC522.length
                end                
                for i=1, n do
                    xx = RC522.dev_read(0x09)
                    back_data[i] = xx
                end
              end
        else
            err = true
        end
    end
    return  err, back_data, back_length 
end

function RC522.anticoll()
    back_data = {}
    serial_number = {}
    serial_number_check = 0    
    RC522.dev_write(0x0D, 0x00)
    serial_number[1] = RC522.act_anticl
    serial_number[2] = 0x20
    err, back_data, back_bits = RC522.card_write(RC522.mode_transrec, serial_number)
    if not err then
        if table.maxn(back_data) == 5 then
            for i, v in ipairs(back_data) do
                serial_number_check = bit.bxor(serial_number_check, back_data[i])
            end           
            if serial_number_check ~= back_data[4] then
                err = true
            end
        else
            err = true
        end
    end   
    return error, back_data
end 

function RC522.calculate_crc(data)
    RC522.clear_bitmask(0x05, 0x04)
    RC522.set_bitmask(0x0A, 0x80)
    for i,v in ipairs(data) do
        RC522.dev_write(0x09, data[i])
    end    
    RC522.dev_write(0x01, RC522.mode_crc)
    i = 255
    while true do
        n = RC522.dev_read(0x05)
        i = i - 1
        if not ((i ~= 0) and not bit.band(n,0x04)) then
            break
        end
    end
    ret_data = {}
    ret_data[1] = RC522.dev_read(0x22)
    ret_data[2] = RC522.dev_read(0x21)
    return ret_data
end

Those are original functions.
And finally, we have an init:

function RC522.init(callback)
    spi.setup(1, spi.MASTER, spi.CPOL_LOW, spi.CPHA_LOW, spi.DATABITS_8, 0)
    gpio.mode(RC522.pin_rst,gpio.OUTPUT)
    gpio.mode(RC522.pin_ss,gpio.OUTPUT)
    gpio.write(RC522.pin_rst, gpio.HIGH) 
    gpio.write(RC522.pin_ss, gpio.HIGH)
    RC522.dev_write(0x01, RC522.mode_reset)
    RC522.dev_write(0x2A, 0x8D)
    RC522.dev_write(0x2B, 0x3E)
    RC522.dev_write(0x2D, 30)
    RC522.dev_write(0x2C, 0)
    RC522.dev_write(0x15, 0x40)
    RC522.dev_write(0x11, 0x3D)
    current = RC522.dev_read(RC522.reg_tx_control)
    if bit.bnot(bit.band(current, 0x03)) then
        RC522.set_bitmask(RC522.reg_tx_control, 0x03)
    end
    print("RC522 Firmware Version: 0x"..string.format("%X", RC522.getFirmwareVersion()))
    RC522.rc_timer:register(100, tmr.ALARM_AUTO, function()
        isTagNear, cardType = RC522.request()
        if isTagNear == true then
            RC522.rc_timer:stop()
            err, serialNo = RC522.anticoll()
            serialNo = RC522.appendHex(serialNo)
            if last == "" and serialNo:len() > 9 then
                message = network_message.prepareMessage() 
                message.event = 'rc522.read'
                message.parameters = {['id'] = serialNo}
                network_message.sendMessage(send_socket, message)
                last = serialNo
                if callback ~= nil then callback(serialNo) else last="" end
            end 
            buf = {}
            buf[1] = 0x50 
            buf[2] = 0
            crc = RC522.calculate_crc(buf)
            table.insert(buf, crc[1])
            table.insert(buf, crc[2])
            err, back_data, back_length = RC522.card_write(rc522.mode_transrec, buf)
            RC522.clear_bitmask(0x08, 0x08)
            RC522.rc_timer:start()
        end
    end)
    RC522.rc_timer:start()
end

return RC522

We moved init routine to its own function. And there we added a timer. To make things more flexible an init function takes a callback function and timer calls it when the card is read.

The timer works in following way, it stops itself, reads card id, checks if the previous read is cleared (so two reads won’t overlap) and if last readΒ  is empty it sends a message with event rc522.read and card id as parameter, store card id as the last read and calls user callback if any. In the case of no callback, the lastΒ variable is cleared at once.
Then the timer is restarted.
If you look carefully you will see a possible deadlock. We do not clear the last read when the callback is set.
User’s function is responsible for handling card and clearing last variable.
This takes care of reading the id from the card and broadcast an event but what about an answer?

Handler

Parsing an answer is a task for handler!
The most important part is a custom function to handle the response. So our handler looks like this:

local rc522_handler = {}
rc522_handler.__index = rc522_handler

setmetatable(rc522_handler, {
    __call = function (cls, ...)
        return cls.new(...)
    end,
})

function rc522_handler.new(callback)
    local self = setmetatable({}, rc522_handler)    
    self.callback = callback
    return self
end   

function rc522_handler:handle(socket, message)
    response = false
    if message ~= nil and message.event ~= nil then
        if message.event == 'rc522.response' then
            if self.callback ~= nil then
                self.callback('lrc522.response', message.parameters)
            end
            response = true
        end
    end

    return response
end

return rc522_handler 

The handler is short and needs a user callback to do something useful.

Putting it all together

We have a module, we have a handler but we need an example.
Our example will work as follow, we will read a card id, broadcast a message and await a response. To help us see a state we will use a triled. Blue is when we read a card and it would change after getting a response or after the timeout.

First, few entries in parameters:


RC522_PIN_RST = 2
RC522_PIN_SS = 4
PIN_RED = 3
PIN_GREEN = 1
PIN_BLUE = 8

BLINK_WIFI_FAILURE = 2

LED_CONFIRM_LEN = 1500

and full main file:

print ("core ready")

network_message = require "network_message"
server_listener = require "server_listener"
rc522_handler = require "rc522_handler"
rc522 = require "rc522"

send_socket = net.createUDPSocket()
last = ""

handler = rc522_handler(function(event, params)   
    if last ~= nil and params.id == last then
        clear_last:stop()
        print(params.id)
        print(params.response)
        triled.clear()
        if params.response == 'OK' then triled.green(true, LED_CONFIRM_LEN) else triled.red(true, LED_CONFIRM_LEN) end        
        last = ""
    end
end)

This handler gets a response (from the server) and turns on the green or red led.

server_listener.add("rc522", handler)
server_listener.start(PORT)

clear_last = tmr.create()
clear_last:register(5000, tmr.ALARM_AUTO, function()
    last = ""
    triled.clear()
    triled.red(true, LED_CONFIRM_LEN)
    clear_last:stop()
end)

When the card is read, a blue led is turned on. If we have no response from the server we need to disable blue and enable red led.

rc522.pin_ss = RC522_PIN_SS
rc522.pin_rst = RC522_PIN_RST
rc522.init(function(cardid) 
    print("Tag Found: "..cardid)    
    triled.clear()
    triled.blue(true)
    clear_last:start()
end)
triled.init()

And here is a callback to the card reader. It enables blue led and starts the timer from the previous paragraph.

Server

What would be a reader without a server to check and identify a card. Our implementation is only for tests πŸ™‚ It randomly returns ERROR or OK:

import socket
import json
import random
import time

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
s.bind(('', 5053))
address = ('192.168.1.255', 5053)

def response_ok(cid):
    tmp = get_packet()
    tmp['parameters']['id'] = cid
    tmp['parameters']['response'] = 'OK'
    return tmp

def response_fail(cid):
    tmp = get_packet()
    tmp['parameters']['id'] = cid
    tmp['parameters']['response'] = 'ERROR'
    return tmp

def get_packet():
    return {
        'protocol': 'iot:1',
        'node': 'computer',
        'chip_id': 'd45656b45afb58b1f0a46',
        'event': 'rc522.response',
        'parameters': {
            'id': '',
            'response': ''
        },
        'targets': [
            'ALL'
        ]
    }

while 1:
    try:
        message, caddress = s.recvfrom(1024)
        message = message.decode()
        message = json.loads(message)
        if message['event'] == 'rc522.read':
            time.sleep(2)
            print("get card ", message['parameters']['id'])
            if random.randint(0,100) > 50:
                response = response_fail(message['parameters']['id'])
            else:
                response = response_ok(message['parameters']['id'])
            msg = json.dumps(response)
            print(msg)
            s.sendto(msg.encode(), address)
            s.sendto(msg.encode(), address)
    except:
        raise

It reads card id, wait for 2 seconds and responds. See that response is sent two times – just for fun πŸ™‚

Case

We have a code, we know how to wire parts so let’s take the last step and finish a prototype. What is missing? A nice case for all scattered parts.
I found cases that maybe do not look perfect but are good enough. From a few a selected case called Z32

How to pack items inside?

The main part is a mini-board. It has sockets for NodeMCU on both sides but only one is used. We made it for stability.
Wires that connect RC522 to the board are soldered to board on one side and have sockets on another side.
Board has two switches, one is connected to D0 – this one stops booting, second, connected to D1, is used by LED and in future will enable a configurator.
And finally, we have a LED that is mounted in the socket.

The NodeMCU fit nicely between a LED socket and a box side. The only problem is with a flip cover, it’s so tight that it won’t open 😦

It was so tight that a 3V connection was broken πŸ™‚ Nice fail.

Summary

We have a module to read a card, we have a handler to err handle response from the server and we have a sample server. All this was combined into a working prototype!

And, like quite often happens after the talk with Client we need to change the flow πŸ™‚ But this in the next part πŸ™‚

 

Advertisements

One comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s