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.
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:
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.
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.
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
);
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.
Tenemos una interfaz que muestra a los empleados con su respectiva tarjeta en caso de que ya cuenten con una. Hay 3 opciones:
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> Assigned ({{employee.rfid_serial}})</span></h4>
<h4 v-else-if="employee.waiting"><span class="badge badge-warning"><i class="fa fa-clock"></i> Waiting... Please read a RFID card</span></h4>
<h4 v-else><span class="badge badge-primary"><i class="fa fa-times"></i> 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.
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):
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:
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.
Hoy te voy a presentar un creador de credenciales que acabo de programar y que…
Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…
En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…
En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…
Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…
En este artículo te voy a enseñar cómo usar un "top level await" esperando a…
Esta web usa cookies.
Ver comentarios
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
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
Hola. Gracias por sus comentarios. Si tiene alguna consulta, solicitud de creación de un programa o solicitud de cambio de software estoy para servirle en https://parzibyte.me/#contacto
Saludos!
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.
Hola. Gracias por sus comentarios. Le deseo éxito en su proyecto
Saludos :)