Registro de Horários em Cartão RFID usando a Raspberry Pi Pico

Registro de Horários em Cartão RFID usando a Raspberry Pi Pico Deixe um comentário

Registro de Horários em Cartão RFID usando a Raspberry Pi Pico

Introdução

Neste artigo vamos ver um sistema que registra automaticamente a data e hora atual em um cartão RFID MIFARE quando ele é aproximado do leitor/gravador. A figura abaixo mostra o diagrama de blocos do nosso projeto:

Diagrama do projeto registro de horários em Cartão RFID usando a Raspberry Pi Pico

A Raspberry Pi Pico fornece a inteligência do nosso sistema, rodando uma aplicação escrita em MicroPython. O módulo RTC DS1307 é usado para manter a data e hora atual. O Leitor (ou mais apropriadamente leitor/gravador) MFRC522 lê e atualiza as informações no cartão RFID MIFARE. Um LED bicolor (verde/vermelho) é usado para indicar o sucesso ou falha das operações. Um botão é usado para colocar o sistema no modo de manutenção, onde é feita a preparação do cartão e a descarga dos dados nele armazenados. Para usar o modo manutenção é necessário ligar um micro à Pi Pico através da USB.

Nos próximos itens vamos conhecer um pouco mais sobre esses componentes.

Raspberry Pi Pico

A “Pi Pico” é uma placa com o poderoso microcontrolador RP2040, que possui respeitáveis capacidades de processamento, memória e entrada e saída. Você vai encontrar detalhes sobre ela em vários artigos aqui no blog, resumo aqui os recursos importantes para o nosso projeto:

  • A interface USB pode ser usada como porta de comunicação (o que vamos usar no modo manutenção)
  • Suporta comunicação serial nos padrões I2C e SPI (usados para conversar com o módulo RTC e o leitor RFID)
  • A maioria dos seus pinos podem ser usados para entrada ou saída digital (o que usaremos para o LED e botão)

Módulo RTC DS1307

Este módulo combina um relógio de tempo real DS1307 (com bateria) e uma memória EEPROM 2432 (que não usaremos neste projeto). Ambos os componentes se comunicam através de I2C (você pode ver aqui um artigo sobre essa comunicação que usa este módulo como exemplo).

Cartão MiFare e Leitor MFRC522

O padrão MiFare de RFID foi desenvolvido para aplicações de bilhetagem. Ele suporta a leitura e escrita de dados em cartões ou tags RFID sem contato, a curta distância, de forma segura. Em um post anterior eu descrevi com mais detalhes o uso de cartões padrão MiFare, o que precisamos saber aqui é que os cartões que vamos usar possuem 1K de memória divididos em 16 setores, cada um com quatro blocos de dezesseis bytes. O último bloco de cada setor é usado para controle de acesso (não vamos usar este recurso neste projeto) e o primeiro bloco do primeiro setor não pode ser alterado. Este primeiro bloco inclui uma identificação única do cartão.

O leitor MFRC522 se comunica com um microcontrolador via SPI e fornece os comandos necessários para detectar a presença de um cartão e ler e escrever na sua memória.

LED Bicolor

O LED que vamos usar é na verdade constituído por dois LEDs, um verde e um vermelho, encapsulados juntos. Estes dois LEDs estão invertidos um em relação ao outro, a cor é definida pelo sentido da tensão aplicada entre os dois pinos.

Material Necessário

Para a parte prática você precisará:

Você pode substituir o LED bicolor por um LED vermelho e um LED verde em paralelo, tomando o cuidado de colocar um invertido em relação a outro.

IMPORTANTE: Como vamos escrever no cartão e reconfigurá-lo, use somente um cartão vazio e que você possa descartar caso algo ocorra errado.

Montagem

A figura abaixo mostra a montagem que vamos utilizar:

Montagem do sistema no Fritzing

Software

O software foi desenvolvido em MicroPython, veja neste artigo como preparar a Raspberry Pi Pico e o seu microcomputador.

A comunicação com o leitor MFRC522 será feito através de uma biblioteca. Baixe o zip de https://github.com/danjperron/micropython-mfrc522 (conforme a figura abaixo), expanda em um diretório e use o Thonny para instalar o arquivo mfrc522.py na Raspberry Pi Pico.

Comunicação com o leitor MFRC522 através de uma biblioteca.

Vamos usar o segundo bloco do primeiro setor do cartão para identificar que ele será usado pela nossa aplicação e para indicar onde será feita a próxima gravação. Vamos gravar as datas e horas em 4 bytes, o permite 4 marcações por bloco. Os dados serão gravados a partir do segundo setor, o que nos fornece uma capacidade de 180 marcações (15 setores x 3 blocos/setor x 4 marcações/bloco). Por simplificação, quando o cartão ficar cheio será necessário reiniciá-lo para uso.

O código completo é esse:

 

# Registra data e hora em cartão (ou token) Mifare

from machine import Pin
from machine import I2C
from mfrc522 import MFRC522
import time
import utime

# Conexões
pinSCK = 2
pinMISO = 4
pinMOSI = 3
pinCS = 1
pinRST = 0
pinBotao = Pin(11, Pin.IN, Pin.PULL_UP)
pinRED = Pin(12, Pin.OUT)
pinGREEN = Pin(13, Pin.OUT)
pinSDA = Pin(14)
pinSCL = Pin(15)

# Classe para controle de LED Bicolor
class LED:
    
    OFF = 0
    RED = 1
    GREEN = 2

    def __init__(self, pinR, pinG):
        self.pinR = pinR
        self.pinG = pinG
        self.write(self.OFF)
        
    def write(self, modo):
        if modo == self.RED:
            self.pinR.high()
            self.pinG.low()
        elif modo == self.GREEN:
            self.pinR.low()
            self.pinG.high()
        else:
            self.pinR.low()
            self.pinG.low()
    

# Classe simples para acesso ao RTC DS1307
class RTC:

    # Converte de BCD para inteiro
    def bcd2num(x):
        return ((x >> 4)*10) + (x & 0x0F)

    # Converte de inteiro para BDC
    def num2bcd(x):
        return ((x // 10)<<4) + (x % 10)

    # Iniciação
    def __init__(self, i2c, addr = 0x68):
        self.i2c = i2c
        self.addr = addr

    # Retorna data e hora no formato time do Unix
    def getTime(self):
        regs = self.i2c.readfrom_mem(self.addr, 0, 7)
        segundo = RTC.bcd2num(regs[0] & 0x7F)
        minuto = RTC.bcd2num(regs[1])
        hora = RTC.bcd2num(regs[2] & 0x3F)
        dia = RTC.bcd2num(regs[4])
        mes = RTC.bcd2num(regs[5])
        ano = RTC.bcd2num(regs[6])
        return time.mktime ((ano+2000,mes,dia,hora,minuto,segundo,0,0))

    # Lê data e hora
    def leRelogio(self):
        regs = self.i2c.readfrom_mem(self.addr, 0, 7)
        segundo = RTC.bcd2num(regs[0] & 0x7F)
        minuto = RTC.bcd2num(regs[1])
        hora = RTC.bcd2num(regs[2] & 0x3F)
        dia = RTC.bcd2num(regs[4])
        mes = RTC.bcd2num(regs[5])
        ano = RTC.bcd2num(regs[6])
        return '%02d/%02d/%02d% 02d:%02d:%02d' % (dia, mes, ano, hora,
                                                  minuto, segundo)

    # Acerta o relogio (dh = ddmmaaHHMMSS)
    def acertaRelogio(self,dh):
        dia = int(dh[0:2])
        mes = int(dh[2:4])
        ano = int(dh[4:6])
        hora = int(dh[6:8])
        minuto = int(dh[8:10])
        segundo = int(dh[10:12])
        regs = bytearray(7)
        regs[0] = RTC.num2bcd(segundo)
        regs[1] = RTC.num2bcd(minuto)
        regs[2] = RTC.num2bcd(hora)
        regs[3] = 1
        regs[4] = RTC.num2bcd(dia)
        regs[5] = RTC.num2bcd(mes)
        regs[6] = RTC.num2bcd(ano)
        self.i2c.writeto_mem(self.addr, 0, regs)

# Classe para encapsular acessos ao cartão
class Cartao:

    # Formato do bloco 1 do setor 0
    #   bytes 0 a 3: marca
    #   byte 4: indice para próxima gravação
    #   bytes 5 a 15: não usados

    chave = [ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF ] # padrão de fábrica
    marca = [ 0x46, 0x4c, 0x4f, 0x50 ]

    # Iniciacao
    def __init__(self, timeout=5000):
        self.card = MFRC522(spi_id=0,sck=2,miso=4,mosi=3,cs=1,rst=0)
        self.card.init()
        self.cartaoAtual = [0]
        self.lidoEm = 0
        self.timeout = timeout
        
    # Testa se tem cartão para registro
    def presente(self):
        # Ver se tem cartão
        (stat, tag_type) = self.card.request(self.card.REQIDL)
        if stat == self.card.OK:
            # Tem um cartão
            (stat, uid) = self.card.SelectTagSN()
            if (stat == self.card.OK) and (uid != self.cartaoAtual):
                # Cartão a ser tratado
                self.cartaoAtual = uid
                self.lidoEm = time.ticks_ms()
                return True
        elif self.cartaoAtual != [0]:
                # Esquecer cartão anterior depois de um tempo
                delta = time.ticks_diff(time.ticks_ms(), self.lidoEm)
                if delta > self.timeout:
                    self.cartaoAtual = [0]
        return False

    # Aguarda cartão para manutenção
    def espera(self):
        timeout = time.ticks_ms() + 5000
        while timeout > time.ticks_ms():
            utime.sleep_ms(50)
            (stat, tag_type) = self.card.request(self.card.REQIDL)
            if stat == self.card.OK:
                (stat, uid) = self.card.SelectTagSN()
                if stat == self.card.OK:
                    self.cartaoAtual = uid
                    return uid
        print ('Desistindo')
        return [0]

    # Libera cartão ao final do uso
    def libera(self):
        self.card.stop_crypto1()

    # Retorna identificação do cartão atual
    def uid(self):
        return self.cartaoAtual
    
    # Verifica se o cartão atual é válido e com espaço
    def valido(self):
        (stat, self.blocoMestre) = self.card.readSectorBlock(
            self.cartaoAtual, 0, 1, self.chave)
        if (stat == self.card.OK) and \
           (self.marca == self.blocoMestre[0:4]):
            self.proxReg = self.blocoMestre[4]
            if self.proxReg < 180:
                return True
        self.libera()
        return False
    
    # Inicia cartão
    def inicia(self):
        dados = []
        dados.extend(self.marca)
        dados.extend([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ])
        ret = self.card.writeSectorBlock(self.cartaoAtual, 
                0, 1, dados, self.chave) == self.card.OK
        self.libera()
        return ret
    
    # Converte indice em setor bloco pos
    def endereca(self, indice):
        return ((indice // 12) + 1, (indice % 12) // 4, (indice % 4) * 4)
    
    # Anota data e hora atual no cartão
    def anotaHora(self):
        ret = False
        (setor, bloco, pos) = self.endereca(self.proxReg)
        (stat, dado) = self.card.readSectorBlock(self.cartaoAtual, 
                       setor, bloco, self.chave)
        if stat == self.card.OK:
            hora = rtc.getTime()
            dado[pos:pos+4] = list(hora.to_bytes(4, 'big'))
            if self.card.writeSectorBlock(self.cartaoAtual, 
                 setor, bloco, dado, self.chave) == self.card.OK:
                self.proxReg = self.proxReg+1
                self.blocoMestre[4] = self.proxReg
                if self.card.writeSectorBlock(self.cartaoAtual, 0, 1,
                   self.blocoMestre, self.chave) == self.card.OK:
                    ret = True
                else:
                    print ('Erro ao atualizar próximo bloco')
            else:
                print ('Erro ao gravar data e hora')
        else:
            print ('Erro ao ler cartão')
        return ret
    
    # Retorna registros no cartão
    def pegaRegistros(self):
        for indice in range(0, self.proxReg):
            (setor, bloco, pos) = self.endereca(indice)
            (stat, dado) = self.card.readSectorBlock(self.cartaoAtual,
                           setor, bloco, self.chave)
            if stat == self.card.OK:
                hora = time.localtime(
                       int.from_bytes(bytes(dado[pos:pos+4]), 'big'))
                yield '{:02d}/{:02d}/{:04d} {:02d}:{:02d}:{:02d}'.format(
                       hora[2],hora[1],hora[0],hora[3],hora[4], hora[5])
            else:
                print ('Erro ao ler cartão')
                break
        cartao.libera()


# Iniciações
cartao = Cartao()
rtc = RTC(I2C(1, scl=pinSCL, sda=pinSDA, freq=100000))
print ('Data e hora atual: '+rtc.leRelogio())
led = LED(pinRED, pinGREEN)

# Tratamento de leitura de cartão
def trataCartao(uid):
    print("Cartão lido {}".format(
        hex(int.from_bytes(bytes(uid),"little",False)).upper()))
    cartao.anotaHora()
    cartao.libera()
    led.write(led.GREEN)
    utime.sleep_ms(500)
    led.write(led.OFF)

# Solicita cartão para manutenção
def solicitaCartao():
    print ('Coloque o cartão perto do leitor')
    return cartao.espera()

# Inicia o cartão para uso
def iniciaCartao():
    uid = solicitaCartao()
    if uid == [0]:
        return
    if cartao.inicia():
        print ('Sucesso')
        led.write(led.GREEN)
    else:
        print ('Erro na gravação')
        led.write(led.RED)
    utime.sleep_ms(500)
    led.write(led.OFF)

def listaCartao():
    uid = solicitaCartao()
    if uid == [0] :
        return
    if cartao.valido():
        for reg in cartao.pegaRegistros():
            print (reg)
        led.write(led.GREEN)
    else:
        print ('Cartão inválido')
        led.write(led.RED)
    utime.sleep_ms(500)
    led.write(led.OFF)

# Modo manutenção
def manutencao():
    print ()
    print ("Modo MANUTENÇÃO")
    print ("I: inicia cartão")
    print ("L: lista dados no cartão")
    print ("Rddmmaahhmmss: acerta relogio")
    print ("F: encerra")
    while True:
        cmd = input('Comando: ').upper()
        if cmd == 'F':
            break
        elif cmd == 'I':
            resp = input('Confirma (S/N)?').upper()
            if resp == 'S':
                iniciaCartao()
        elif cmd == 'L':
            listaCartao()
        elif cmd.startswith('R') and len(cmd)==13:
            rtc.acertaRelogio(cmd[1:])
            print ('Data e hora atual: '+rtc.leRelogio())
    while pinBotao.value() == 0:
        utime.sleep_ms(50)

# Loop principal
try:
    while True:
        # Testa se apertou o botão
        if pinBotao.value() == 0:
            manutencao()
        # Verifica se leu um cartão
        if cartao.presente():
            if cartao.valido():
                trataCartao(cartao.uid())
            else:
                print ('Cartão inválido')
                led.write (led.RED)
                utime.sleep_ms(500)
                led.write(led.OFF)
        # Dá um tempo
        utime.sleep_ms(50)
except KeyboardInterrupt:
    print ('Fim')
except Exception as e:
    print (e)

Operação

Após a carga e execução da aplicação, aperte o botão para colocar no modo de manutenção e digite no shell “Rddmmaahhmmss [Enter]” (dia mês ano hora minuto segundo) para colocar a data e hora atuais no módulo de relógio.

Em seguida inicie os cartões que serão usados:

  • Digite “I [Enter]”
  • Aproxime um cartão da antena do leitor
  • Quando solicitado, digite “S [Enter]” para confirmar a iniciação do cartão ou N para desistir
  • Aguarde o LED piscar verde, indicando que a iniciação foi bem sucedida
  • Repita os passos anteriores para todos os cartões
  • Ao final digite “F [Enter]” para sair do modo manutenção

Para registrar a data e horário atuais basta aproximar o cartão da antena, o LED piscará verde para indicar que o registro foi bem sucedido. Se o LED piscar vermelho algo deu errado (você pode ver no shell o motivo).

Para ler os registros em um cartão, aperte o botão para entrar no modo manutenção, digite “L [Enter]” e aproxime o cartão da antena.

A imagem abaixo exemplifica o uso do sistema:

Uso do sistema de registro de Horários em Cartão RFID

Conclusão

Este projeto mostrou como usar a Raspberry Pi Pico com diversos dispositivos que podem ser úteis em outros projetos: botão, LED bicolor, módulo RTC e leitor/gravador de RFID.

Você pode usar o projeto exatamente como apresentado ou aperfeiçoá-lo. Algumas sugestões:

  • Substituir o LED por um display (alfanumérico ou gráfico) para um interface com o operador mais sofisticada
  • No lugar do botão usar um (ou mais) tags RFID para entrar no modo de manutenção. Você pode usar a EEPROM do módulo RTC para guardar a identificação destes tags.
  • Usar os blocos do cartão como uma fila circular, mantendo o número do último bloco enviado ao PC e o número do bloco onde será gravada a próxima leitura.

Então, gostou do artigo? Pretende montar o projeto ou fazer algo derivado dele? Dúvidas? Use os comentários abaixo para conta para nós!

 

Faça seu comentário

Acesse sua conta e participe