Sistema de registro de asistencia con tarjetas RFID

Hoy te mostraré un software que acabo de programar usando PHP, MySQL, tarjetas RFID y una tarjeta ESP8266. Este programa es totalmente gratuito y open source.

Se trata de un sistema de registro de asistencia de empleados en PHP, basado en otro sistema que recién publiqué, pero ahora con una característica adicional: la asistencia también puede ser tomada usando tarjetas RFID.

De este modo se pueden asignar tarjetas RFID a los empleados (incluido totalmente en el sistema) y también pasar la asistencia de manera automática al usar estas tarjetas.

Con ligeras modificaciones incluso se podría hacer un sistema para el registro del tiempo, entradas y salidas a determinado lugar, etcétera.

A través del post te mostraré cómo usar el sistema, descargarlo, y sobre todo cómo está programado.

Lecturas recomendadas

Si bien esto no es un “copiar y pegar”, sí que toma varios aspectos de varios posts que ya he expuesto, los cuales te animo a leer:

  • Leer serial de RFID con ESP8266: te muestro el circuito de conexión y los requisitos para leer las tarjetas RFID. En este caso vamos a usar la tarjeta NodeMCU ESP8266 que puede ser conectada al lector RC522
  • Enviar lectura de RFID a PHP: una vez que ya hemos leído el serial del RFID, usamos el módulo HTTP para enviarlo a nuestro servidor web.
  • Sistema de asistencias: el sistema en el que se basa el software gratuito que te muestro en este post.

Te repito que deberías leerlos, pues aquí solo explicaré lo que cambia desde esos sistemas hasta lo presentado en este artículo. Eso sí, te dejaré el código completo a lo largo del post, pero lo digo para que puedas aprender el funcionamiento.

Dicho eso, comencemos.

Código del lector RFID

Conexión lector RFID con NodeMCU ESP8266 – Circuito

Veamos primero el código de la tarjeta ESP8266. En este caso queda así:

/*
  ____          _____               _ _           _       
 |  _ \        |  __ \             (_) |         | |      
 | |_) |_   _  | |__) |_ _ _ __ _____| |__  _   _| |_ ___ 
 |  _ <| | | | |  ___/ _` | '__|_  / | '_ \| | | | __/ _ \
 | |_) | |_| | | |  | (_| | |   / /| | |_) | |_| | ||  __/
 |____/ \__, | |_|   \__,_|_|  /___|_|_.__/ \__, |\__\___|
         __/ |                               __/ |        
        |___/                               |___/         
    
____________________________________
/ Si necesitas ayuda, contáctame en \
\ https://parzibyte.me               /
 ------------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
Creado por Parzibyte (https://parzibyte.me).
------------------------------------------------------------------------------------------------
Si el código es útil para ti, puedes agradecerme siguiéndome: https://parzibyte.me/blog/sigueme/
Y compartiendo mi blog con tus amigos
También tengo canal de YouTube: https://www.youtube.com/channel/UCroP4BTWjfM0CkGB6AFUoBg?sub_confirmation=1
------------------------------------------------------------------------------------------------
*/
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <SPI.h>
#include <MFRC522.h>

/*
In the ESP8266, D3 pin is RST_PIN and
D4 pin is SS_PIN
*/
#define RST_PIN D3
#define SS_PIN D4

MFRC522 reader(SS_PIN, RST_PIN);
MFRC522::MIFARE_Key key;

// Credentials to connect to the wifi network
const char *ssid = "SSID";
const char *password = "PASSWORD";
/*
The ip or server address. If you are on localhost, put your computer's IP (for example http://192.168.1.65)
If the server is online, put the server's domain for example https://parzibyte.me
*/
const String SERVER_ADDRESS = "http://192.168.1.77/asistencia-php-rfid";
void setup()
{

  // Connect to wifi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(1000);
  }

  SPI.begin();

  reader.PCD_Init();
  // Just wait some seconds...
  delay(4);
  // Prepare the security key for the read and write functions.
  // Normally it is 0xFFFFFFFFFFFF
  // Note: 6 comes from MF_KEY_SIZE in MFRC522.h
  for (byte i = 0; i < 6; i++)
  {
    key.keyByte[i] = 0xFF; //keyByte is defined in the "MIFARE_Key" 'struct' definition in the .h file of the library
  }
}

void loop()
{
  // If not connected, we don't need to read anything, that would be unnecessary
  if (WiFi.status() != WL_CONNECTED)
  {
    return;
  }
  // But, if there is a connection we check if there's a new card to read

  // Reset the loop if no new card present on the sensor/reader. This saves the entire process when idle.
  if (!reader.PICC_IsNewCardPresent())
  {
    return;
  }

  // Select one of the cards. This returns false if read is not successful; and if that happens, we stop the code
  if (!reader.PICC_ReadCardSerial())
  {
    return;
  }

  /*
    At this point we are sure that: there is a card that can be read, and there's a
    stable connection. So we read the id and send it to the server
  */

  String serial = "";
  for (int x = 0; x < reader.uid.size; x++)
  {
    // If it is less than 10, we add zero
    if (reader.uid.uidByte[x] < 0x10)
    {
      serial += "0";
    }
    // Transform the byte to hex
    serial += String(reader.uid.uidByte[x], HEX);
    // Add a hypen
    if (x + 1 != reader.uid.size)
    {
      serial += "-";
    }
  }
  // Transform to uppercase
  serial.toUpperCase();

  // Halt PICC
  reader.PICC_HaltA();
  // Stop encryption on PCD
  reader.PCD_StopCrypto1();

  HTTPClient http;

  // Send the tag id in a GET param
  const String full_url = SERVER_ADDRESS + "/rfid_register.php?serial=" + serial;
  http.begin(full_url);

  // Make request
  int httpCode = http.GET();
  if (httpCode > 0)
  {
    if (httpCode == HTTP_CODE_OK)
    {

      // const String &payload = http.getString().c_str(); //Get the request response payload
    }
    else
    {
    }
  }
  else
  {
  }

  http.end(); //Close connection
}

Como puedes ver, lo único que hace la tarjeta es enviar el valor a través de la red, en el endpoint /rfid_register.php pasándole el serial como parámetro de la URL.

Ya en el servidor nos vamos a encargar de hacer lo pertinente de acuerdo a la situación. Para este caso habrá dos modos: el modo de lectura y modo de emparejamiento.

En el modo de emparejamiento, el servidor va a tener ya un id de empleado con el que va e relacionar o emparejar el serial de la tarjeta RFID, así que tan pronto se le envíe un serial, va a realizar el registro de que la tarjeta le pertenece al empleado.

Por otro lado, en el modo de lectura simplemente se leerá el serial y en caso de que haya un empleado registrado con esa tarjeta (emparejado anteriormente usando el otro modo) se le pondrá asistencia para esa fecha.

De este modo dejamos la mayor parte del funcionamiento en el servidor, para que podamos dejar que el lector y la ESP8266 se encarguen de una sola cosa.

Base de datos

En cuanto a la base de datos MySQL que usa este sistema de asistencias con tarjetas RFID, se usan 3 tablas. La primera es la de empleados, la segunda es la de la asistencia de empleados y la tercera es la que relaciona las tarjetas RFID con cada empleado.

CREATE TABLE IF NOT EXISTS employees(
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL
);

CREATE TABLE employee_attendance(
    employee_id BIGINT UNSIGNED NOT NULL,
    date VARCHAR(10) NOT NULL,
    status ENUM('presence', 'absence'),
    FOREIGN KEY (employee_id) REFERENCES employees(id) ON UPDATE CASCADE ON DELETE CASCADE
);

CREATE TABLE employee_rfid(
    employee_id BIGINT UNSIGNED NOT NULL,
    rfid_serial VARCHAR(11),
    FOREIGN KEY (employee_id) REFERENCES employees(id) ON UPDATE CASCADE ON DELETE CASCADE
);

Funciones útiles

A las funciones que ya estaban en el sistema de asistencias, le he agregado otras adicionales para el manejo de las tarjetas RFID, el modo del lector, etcétera.

Es importante mencionar los modos del lector, pues en este caso el modo se guarda en un archivo de texto (que es creado de manera automática). Además, el ID del empleado con el que se va a emparejar también es guardado en un archivo de texto.

<?php
if (!defined("RFID_STATUS_FILE")) {
    define("RFID_STATUS_FILE", "rfid_status");
}
if (!defined("RFID_STATUS_READING")) {
    define("RFID_STATUS_READING", "r");
}
if (!defined("RFID_STATUS_PAIRING")) {
    define("RFID_STATUS_PAIRING", "p");
}
if (!defined("PAIRING_EMPLOYEE_ID_FILE")) {
    define("PAIRING_EMPLOYEE_ID_FILE", "pairing_employee_id_file");
}

function getEmployeesWithRfid()
{
    $query = "SELECT employee_id, rfid_serial FROM employee_rfid";
    $db = getDatabase();
    $statement = $db->query($query);
    return $statement->fetchAll();
}

function onRfidSerialRead($rfidSerial)
{
    if (getReaderStatus() === RFID_STATUS_PAIRING) {
        pairEmployeeWithRfid($rfidSerial, getPairingEmployeeId());
        setReaderStatus(RFID_STATUS_READING);
    } else {
        $employee = getEmployeeByRfidSerial($rfidSerial);
        if ($employee) {
            saveEmployeeAttendance($employee->id);
        }
    }
}
function saveEmployeeAttendance($employeeId)
{

    $date = date("Y-m-d");
    $status = "presence";
    $query = "INSERT INTO employee_attendance(employee_id, date, status) VALUES (?, ?, ?)";
    $db = getDatabase();
    $statement = $db->prepare($query);
    return $statement->execute([$employeeId, $date, $status]);
}

function setReaderForEmployeePairing($employeeId)
{
    setReaderStatus(RFID_STATUS_PAIRING);
    setPairingEmployeeId($employeeId);
}

function setPairingEmployeeId($employeeId)
{
    file_put_contents(PAIRING_EMPLOYEE_ID_FILE, $employeeId);
}

function getPairingEmployeeId()
{
    return file_get_contents(PAIRING_EMPLOYEE_ID_FILE);
}

function pairEmployeeWithRfid($rfidSerial, $employeeId)
{
    removeRfidFromEmployee($rfidSerial);
    $query = "INSERT INTO employee_rfid(employee_id, rfid_serial) VALUES (?, ?)";
    $db = getDatabase();
    $statement = $db->prepare($query);
    return $statement->execute([$employeeId, $rfidSerial]);
}

function removeRfidFromEmployee($rfidSerial)
{
    $query = "DELETE FROM employee_rfid WHERE rfid_serial = ?";
    $db = getDatabase();
    $statement = $db->prepare($query);
    return $statement->execute([$rfidSerial]);
}

function getEmployeeByRfidSerial($rfidSerial)
{
    $query = "SELECT e.id, e.name FROM employees e INNER JOIN employee_rfid
    ON employee_rfid.employee_id = e.id
    WHERE employee_rfid.rfid_serial = ?";

    $db = getDatabase();
    $statement = $db->prepare($query);
    $statement->execute([$rfidSerial]);
    return $statement->fetchObject();
}
function getEmployeeRfidById($employeeId)
{
    $query = "SELECT rfid_serial FROM employee_rfid WHERE employee_id = ?";
    $db = getDatabase();
    $statement = $db->prepare($query);
    $statement->execute([$employeeId]);
    return $statement->fetchObject();
}

function getReaderStatus()
{
    return file_get_contents(RFID_STATUS_FILE);
}

function setReaderStatus($newStatus)
{
    if (!in_array($newStatus, [RFID_STATUS_PAIRING, RFID_STATUS_READING])) {
        return;
    }

    file_put_contents(RFID_STATUS_FILE, $newStatus);
}

La función más importante es onRfidSerialRead que indica un evento: el evento del serial leído. En este caso puedes ver que si el modo es emparejamiento, se empareja el empleado (obtenido de otra función) con el serial recién leído. Además, una vez leído se pasa al modo lectura.

Por otro lado, si el modo es lectura, entonces se obtiene el empleado ligado a esa tarjeta y se le pone asistencia para esa fecha.

Emparejar empleado con tarjeta RFID

Emparejamiento de tarjeta RFID con empleado – Sistema de asistencia con PHP y MySQL

Tenemos una interfaz que muestra a los empleados con su respectiva tarjeta en caso de que ya cuenten con una. Hay 3 opciones:

  1. Asignada: el empleado ya tiene una tarjeta, al presionar el botón se desasocia la tarjeta RFID del empleado.
  2. Esperando: pone al empleado listo para que, en la siguiente lectura, se le asigne la tarjeta RFID.
  3. No asignada: muestra que el empleado no tiene ninguna tarjeta, por lo que da la opción de asignarla. En caso de que se inicie la asignación, se pasa al modo de espera.

Nota: si una tarjeta RFID ya está usada por otro empleado y ésta se asigna a uno nuevo, se eliminará la asociación que tenía con el primero. Es decir, la tarjeta le pertenece al último que la haya registrado como suya.

He usado Vue.js para manejar este apartado, haciendo llamadas AJAX. El método más complejo de entender es el de esperar a que se asigne la tarjeta, pero lo único que se hace es consultar cada determinado tiempo si el empleado ya tiene asignado un serial, en ese caso, se indica con un toast y se refresca la lista.

De cualquier modo, el código queda así:

<?php
/*

  ____          _____               _ _           _       
 |  _ \        |  __ \             (_) |         | |      
 | |_) |_   _  | |__) |_ _ _ __ _____| |__  _   _| |_ ___ 
 |  _ <| | | | |  ___/ _` | '__|_  / | '_ \| | | | __/ _ \
 | |_) | |_| | | |  | (_| | |   / /| | |_) | |_| | ||  __/
 |____/ \__, | |_|   \__,_|_|  /___|_|_.__/ \__, |\__\___|
         __/ |                               __/ |        
        |___/                               |___/         
    
____________________________________
/ Si necesitas ayuda, contáctame en \
\ https://parzibyte.me               /
 ------------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
Creado por Parzibyte (https://parzibyte.me).
------------------------------------------------------------------------------------------------
Si el código es útil para ti, puedes agradecerme siguiéndome: https://parzibyte.me/blog/sigueme/
Y compartiendo mi blog con tus amigos
También tengo canal de YouTube: https://www.youtube.com/channel/UCroP4BTWjfM0CkGB6AFUoBg?sub_confirmation=1
------------------------------------------------------------------------------------------------
*/ ?>
<?php
include_once "header.php";
include_once "nav.php";
?>
<div class="row" id="app">
    <div class="col-12">
        <h1 class="text-center">RFID Pairing</h1>
    </div>
    <div class="col-12">
        <div class="table-responsive">
            <table class="table">
                <thead>
                    <tr>
                        <th>
                            Employee
                        </th>
                        <th>
                            RFID serial
                        </th>
                        <th>
                            Actions
                        </th>
                    </tr>
                </thead>
                <tbody>
                    <tr v-for="employee in employees">
                        <td>{{employee.name}}</td>
                        <td>

                            <h4 v-if="employee.rfid_serial"><span class="badge badge-success"><i class="fa fa-check"></i>&nbsp;Assigned ({{employee.rfid_serial}})</span></h4>
                            <h4 v-else-if="employee.waiting"><span class="badge badge-warning"><i class="fa fa-clock"></i>&nbsp;Waiting... Please read a RFID card</span></h4>
                            <h4 v-else><span class="badge badge-primary"><i class="fa fa-times"></i>&nbsp;Not assigned</span></h4>
                        </td>
                        <td>
                            <button @click="removeRfidCard(employee.rfid_serial)" v-if="employee.rfid_serial" class="btn btn-danger">Remove</button>
                            <button v-else-if="employee.waiting" @click="cancelWaitingForPairing" class="btn btn-warning">Cancel</button>
                            <button @click="assignRfidCard(employee)" v-else class="btn btn-info">Assign</button>
                        </td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>
</div>
<script src="js/vue.min.js"></script>
<script src="js/vue-toasted.min.js"></script>
<script>
    Vue.use(Toasted);
    let shouldCheck = true;
    const CHECK_PAIRING_EMPLOYEE_INTERVAL = 1000;
    const UNSET_STATUS = "unset";
    new Vue({
        el: "#app",
        data: () => ({
            employees: [],
            date: "",
        }),
        async mounted() {
            await this.setReaderForReading();
            await this.refreshEmployeesList();
        },
        methods: {
            async removeRfidCard(rfidSerial) {
                await fetch("./remove_rfid_card.php?rfid_serial=" + rfidSerial);
                this.$toasted.show("RFID removed", {
                    position: "top-left",
                    duration: 1000,
                });
                await this.refreshEmployeesList();
            },
            async cancelWaitingForPairing() {
                shouldCheck = false;
                await this.setReaderForReading();
            },
            async setReaderForReading() {
                await fetch("./set_reader_for_reading.php");
            },
            async assignRfidCard(employee) {
                shouldCheck = true;
                const employeeId = employee.id;
                employee.waiting = true;
                await fetch("./set_reader_for_pairing.php?employee_id=" + employeeId);
                this.checkIfEmployeeHasJustAssignedRfid(employee);
            },
            async checkIfEmployeeHasJustAssignedRfid(employee) {
                const r = await fetch("./get_employee_rfid_serial_by_id.php?employee_id=" + employee.id);
                const serial = await r.json();
                if (!shouldCheck) {
                    employee.waiting = false;
                    return;
                }
                if (serial) {
                    this.$toasted.show("RFID assigned!", {
                        position: "top-left",
                        duration: 1000,
                    });
                    await this.setReaderForReading();
                    await this.refreshEmployeesList();
                } else {
                    setTimeout(() => {
                        this.checkIfEmployeeHasJustAssignedRfid(employee);
                    }, CHECK_PAIRING_EMPLOYEE_INTERVAL);
                }
            },
            async refreshEmployeesList() {
                // Get all employees
                let response = await fetch("./get_employees_ajax.php");
                let employees = await response.json();
                // Set rfid_serial by default: null
                let employeeDictionary = {};
                employees = employees.map((employee, index) => {
                    employeeDictionary[employee.id] = index;
                    return {
                        id: employee.id,
                        name: employee.name,
                        rfid_serial: null,
                        waiting: false,
                    }
                });
                // Get RFID data, if any
                response = await fetch(`./get_employees_with_rfid.php`);
                let rfidData = await response.json();
                // Refresh rfid data in each employee, if any
                rfidData.forEach(rfidDetail => {
                    let employeeId = rfidDetail.employee_id;
                    if (employeeId in employeeDictionary) {
                        let index = employeeDictionary[employeeId];
                        employees[index].rfid_serial = rfidDetail.rfid_serial;
                    }
                });
                // Let Vue do its magic ;)
                this.employees = employees;
            }
        },
    });
</script>
<?php
include_once "footer.php";

Por cierto, se usa un algoritmo similar al de la asistencia. Primero se obtienen todos los empleados, y luego los datos de las tarjetas. Finalmente se recorren los datos de las tarjetas y en caso de que haya una tarjeta para un empleado, se le sobrepone ese valor.

También es importante notar que usamos v-if propio de Vue para mostrar los botones.

Pasar asistencia con RFID

Ahora veamos el archivo que guarda la asistencia cuando el empleado la pasa por el lector RFID. En este caso es el evento que mencioné anteriormente. La función es:

<?php
function saveEmployeeAttendance($employeeId)
{
    $date = date("Y-m-d");
    deleteEmployeeAttendanceByIdAndDate($date, $employeeId);
    $status = "presence";
    $query = "INSERT INTO employee_attendance(employee_id, date, status) VALUES (?, ?, ?)";
    $db = getDatabase();
    $statement = $db->prepare($query);
    return $statement->execute([$employeeId, $date, $status]);
}

Al leer la tarjeta vamos a recibir el id del empleado (obtenida de la otra función). Primero eliminamos todos los datos que haya para ese empleado en esa fecha (por si anteriormente tenía otro valor) y después de eso se inserta la asistencia con el estado de presencia.

Por cierto, este valor ya se reflejará al pasar asistencia (la opción ya estará seleccionada):

Asistencia tomada con tarjeta RFID usando PHP y MySQL

Aunque obviamente en caso de un error, se puede usar el sistema de asistencias para modificar ese apartado. Las tarjetas solo sirven como ayuda.

Y también aparecerá en el reporte de asistencia:

Reporte de asistencia – Sistema con PHP y MySQL

Poniendo todo junto

En un escenario perfecto, se registran todos los empleados, después se les asigna una tarjeta y cada uno pasa la tarjeta por el lector día con día, para así llevar un control de asistencia completo.

Por cierto, en caso de que no te hayas dado cuenta; las tarjetas son reutilizables. Por lo que si se elimina un empleado, su tarjeta puede ser reutilizada por otro.

Esto puede ser una ventaja o desventaja dependiendo de como lo veas, pero a como yo lo veo, puedes reutilizar su tarjeta y así evitar desperdiciarlas.

Recuerda que el sistema ya tiene toda la gestión de empleados, conexión a base de datos, etcétera. Aquí básicamente estoy mostrando las modificaciones que le hice al sistema que presenté anteriormente.

Te dejo el código fuente completo (el de C++ es el código de la tarjeta) en mi GitHub. También te dejo más posts sobre PHP, ESP8266 y sistemas que he creado.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.

5 comentarios en “Sistema de registro de asistencia con tarjetas RFID”

  1. Hola Parzibyte, que se tendría que modificar en el código php y en vez de ser un lector rc522 fuera un lector usb de rfid conectado por usb?.
    gracias
    saludos

  2. hola el proyecto es muy interesante pero que modificacion se podria realizar si quieres poner una antena de mayor alcance long rage pero esas antenas trabajan con puerto rs232, y no con lector r522

  3. Hola Parzibyte, excelente tutorial, muchas gracias por compartirlo, estuve trabajando en un proyecto parecido, la aplicacion electronica era otra pero si tengo una intranet web escrito en php y como base de datos mysql tambien, lo que yo intento realizar es que mi cliente socket sea mi aplicacion web, o sea que inicia la comunicacion o quien realiza peticiones es mi web al modulo server esp8266 y este responde, estoy en eso y desarrollandolo, este tutorial me sirve de base, gracias por eso, anteriormente escribi una web en java y use java sockets con la web como cliente socket y funcionó aparentemente bien, mi aplicacion web java corria en mi localhost con mi laptop como host conectandome a internet via wifi con una ssid de mi operador local, en el lado del servidor esp8266 con el mismo ssid y no funcionaba, lei que el servidor no era visible para mi aplicacion, y que ambas tenian que estar en la misma red y con privilegios.

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *