Komponenty a zapojenie RC522

RC522 je populárna RFID čítačka pracujúca na 13,56 MHz (ISO 14443A). Komunikuje cez SPI rozhranie. Kompatibilná s bežnými MIFARE kartami a kľúčenkami.

RC522 pinESP32 pin
SDA (SS)GPIO5
SCKGPIO18
MOSIGPIO23
MISOGPIO19
RSTGPIO22
GNDGND
3.3V3.3V

ESP32 kód — čítanie UID karty

Knižnica MFRC522 od miguelbalboa je štandard pre Arduino/ESP32. Inštaluj cez Library Manager.

#include <SPI.h>
#include <MFRC522.h>
#include <WiFi.h>
#include <HTTPClient.h>

#define SS_PIN  5
#define RST_PIN 22

MFRC522 rfid(SS_PIN, RST_PIN);

const char* API_URL = "https://api.gear.sk/api/attendance";
const char* API_KEY = "tajny-kluc";
const char* READER_ID = "vstup-hlavny";

String getUID(MFRC522::Uid uid) {
    String result = "";
    for (byte i = 0; i < uid.size; i++) {
        result += (uid.uidByte[i] < 0x10 ? "0" : "");
        result += String(uid.uidByte[i], HEX);
    }
    result.toUpperCase();
    return result;
}

void sendScan(String uid) {
    HTTPClient http;
    http.begin(API_URL);
    http.addHeader("Content-Type", "application/json");
    http.addHeader("X-API-Key", API_KEY);

    String body = "{\"uid\":\"" + uid + "\",\"reader\":\"" + READER_ID + "\"}";
    int code = http.POST(body);

    if (code == 200) {
        // Zelené LED / zvukový signál = OK
        Serial.println("Scan OK: " + uid);
    } else {
        // Červené LED = neznáma karta alebo chyba
        Serial.println("Scan FAIL: " + String(code));
    }
    http.end();
}

void setup() {
    SPI.begin();
    rfid.PCD_Init();
    WiFi.begin("SSID", "PASS");
    while (WiFi.status() != WL_CONNECTED) delay(500);
    Serial.println("Pripraveny");
}

void loop() {
    if (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial()) {
        delay(50);
        return;
    }

    String uid = getUID(rfid.uid);
    sendScan(uid);
    rfid.PICC_HaltA();
    rfid.PCD_StopCrypto1();
    delay(2000); // cooldown — zabraňuje duplicitným scanom
}

Laravel backend — migrácie a modely

// database/migrations/create_rfid_cards_table.php
Schema::create('rfid_cards', function (Blueprint $table) {
    $table->id();
    $table->string('uid', 20)->unique();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->boolean('active')->default(true);
    $table->timestamps();
});

// database/migrations/create_attendance_logs_table.php
Schema::create('attendance_logs', function (Blueprint $table) {
    $table->id();
    $table->foreignId('rfid_card_id')->constrained();
    $table->foreignId('user_id')->constrained();
    $table->string('reader_id');
    $table->enum('type', ['in', 'out']);
    $table->timestamp('scanned_at')->useCurrent();
    $table->index(['user_id', 'scanned_at']);
});

Controller — príjem scanu

// app/Http/Controllers/AttendanceController.php
class AttendanceController extends Controller
{
    public function scan(Request $request): JsonResponse
    {
        $request->validate([
            'uid'    => 'required|string|max:20',
            'reader' => 'required|string',
        ]);

        $card = RfidCard::with('user')
            ->where('uid', strtoupper($request->uid))
            ->where('active', true)
            ->first();

        if (!$card) {
            return response()->json(['status' => 'unknown'], 403);
        }

        // Zisti typ: ak posledný záznam je 'in', teraz je 'out' a naopak
        $last = AttendanceLog::where('user_id', $card->user_id)
            ->latest('scanned_at')
            ->first();
        $type = (!$last || $last->type === 'out') ? 'in' : 'out';

        AttendanceLog::create([
            'rfid_card_id' => $card->id,
            'user_id'      => $card->user_id,
            'reader_id'    => $request->reader,
            'type'         => $type,
        ]);

        return response()->json([
            'status' => 'ok',
            'name'   => $card->user->name,
            'type'   => $type,
        ]);
    }
}

Laravel routy

// routes/api.php
use App\Http\Controllers\AttendanceController;
use App\Http\Controllers\AttendanceExportController;

Route::middleware('api.key')->group(function () {
    Route::post('/attendance', [AttendanceController::class, 'scan']);
});

// Web dashboard a export — chránený Sanctum autentifikáciou
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/attendance/export', [AttendanceExportController::class, 'export']);
});

Middleware — overenie X-API-Key

// app/Http/Middleware/ApiKeyMiddleware.php
class ApiKeyMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        if ($request->header('X-API-Key') !== config('iot.rfid_key')) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }
        return $next($request);
    }
}

// bootstrap/app.php (Laravel 11+)
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias(['api.key' => ApiKeyMiddleware::class]);
})

CSV export pre mzdové účtovníctvo

// app/Http/Controllers/AttendanceExportController.php
public function export(Request $request): StreamedResponse
{
    $from = $request->date('from', 'Y-m-d', now()->startOfMonth());
    $to   = $request->date('to',   'Y-m-d', now()->endOfMonth());

    $logs = AttendanceLog::with('user')
        ->whereBetween('scanned_at', [$from, $to])
        ->orderBy('user_id')
        ->orderBy('scanned_at')
        ->get();

    return response()->streamDownload(function () use ($logs) {
        $handle = fopen('php://output', 'w');
        fputcsv($handle, ['Meno', 'Typ', 'Čítačka', 'Čas']);
        foreach ($logs as $log) {
            fputcsv($handle, [
                $log->user->name,
                $log->type === 'in' ? 'Príchod' : 'Odchod',
                $log->reader_id,
                $log->scanned_at->format('d.m.Y H:i:s'),
            ]);
        }
        fclose($handle);
    }, 'dochadzka-' . $from->format('Y-m') . '.csv', [
        'Content-Type' => 'text/csv; charset=UTF-8',
    ]);
}

Registrácia novej karty

// Admin rozhranie — registrácia karty k zamestnancovi
// POST /admin/rfid-cards
public function register(Request $request): RedirectResponse
{
    $request->validate([
        'uid'     => 'required|string|unique:rfid_cards,uid',
        'user_id' => 'required|exists:users,id',
    ]);

    RfidCard::create([
        'uid'     => strtoupper($request->uid),
        'user_id' => $request->user_id,
        'active'  => true,
    ]);

    return redirect()->back()->with('success', 'Karta zaregistrovaná.');
}

Rozšírenia systému