Practical case: NFC control with Raspberry Pi 4

Practical case: NFC control with Raspberry Pi 4 — hero

Objective and use case

What you’ll build: A Raspberry Pi 4 Model B prototype that reads NFC cards through a PN532 HAT, checks each UID against an allowlist, timestamps every attempt with a DS3231 RTC, and switches to an alarm state when an unauthorized tag is detected. The system is designed for fast local decisions, with typical card-read handling in under 200 ms and low idle load on the Pi.

Why it matters / Use cases

  • Small workshop or lab cabinet access control: Mount the reader on a tool cabinet, electronics drawer, or project locker so only approved student or staff cards unlock access.
  • Educational entry alarm for a makerspace corner: Unknown cards can trigger a software alarm flag in near real time, suitable for later connection to a buzzer, relay, LED beacon, or webhook notifier.
  • Reliable event logging even without internet: The DS3231 maintains accurate time offline, so denied and granted attempts still get usable timestamps during Wi-Fi outages or isolated lab operation.
  • Simple audit trail for shared equipment: Store events in CSV or JSON with card UID, timestamp, and result to review who attempted access and when.
  • Low-overhead edge prototype: This runs comfortably on a Raspberry Pi 4 with minimal CPU demand and effectively 0% GPU usage, making it practical for always-on monitoring.

Expected outcome

  • A working NFC access checker that classifies presented tags as authorized or unauthorized.
  • Accurate RTC-backed logs for every scan, including offline sessions and reboots.
  • A software alarm state that activates on denied access and can be extended to physical outputs.
  • A baseline end-to-end response time of about 100-200 ms per scan, depending on polling interval and storage writes.
  • A reusable starter project for cabinet locks, lab assets, attendance checkpoints, or entry-alert demos.

Audience: Students, makers, and beginner embedded/Linux developers building access-control demos; Level: Beginner to intermediate

Architecture/flow: PN532 reads NFC UID → Raspberry Pi app checks local allowlist → DS3231 provides timestamp → event is written to CSV/JSON log → authorized scan marks access granted, unauthorized scan sets alarm state.

Educational validation note

Before publication, this case passed the Prometeo automated validation gate with status PASS. The validator checked the code blocks, article structure, copy/paste-safe commands and consistency with the supported device catalog.

Published validation evidence

  • Automatic result: PASS.
  • Parsed structure: 48 sections, 1 tables and 29 code blocks detected in the published content.
  • Checked code: 2 Python/py_compile, 23 Bash/copy-paste checks.
  • Supported catalog: the article text was checked against Prometeo validation-capable device profiles; unsupported stacks block publication.
  • Report findings: no blocking findings.

This validation confirms syntax and tool compatibility for the published material, but it does not replace physical testing on your exact hardware, wiring and runtime environment.

Educational safety note

This project is an educational access-control prototype, not a certified security system. Keep these limits in mind:

  • Do not rely on it as the only protection for valuable property, critical infrastructure, or personal safety.
  • The tutorial focuses on learning interfaces, logging, and access logic.
  • Do not connect mains voltage directly to the Raspberry Pi or breadboard wiring.
  • If you later add a siren, lock, or relay, use only properly isolated low-voltage modules and follow the module documentation.
  • Do not assume the alarm output in this tutorial can drive a real lock or siren directly.
  • The current tutorial uses a software alarm state and console indication.
  • Protect the Raspberry Pi from wiring mistakes.
  • Use 3.3 V-compatible peripherals and double-check pin labels before power-up.
  • The NFC mechanism shown here is not hardened against cloning, replay, tampering, or physical bypass.
  • Treat it as a teaching platform, not a secure commercial entry system.
  • If you install it near a real door, ensure there is always a safe manual override and lawful use.
  • Never create a setup that could trap people or block emergency exit paths.

Conceptual block diagram

High-level view: what enters the system, what each block processes, and what comes out.

Functional architecture

PN532 reads NFC UID

Raspberry Pi app checks local allowlist

DS3231 provides timestamp

event is written to CSV/JSON log

authorized scan marks access granted, una…

Conceptual signal and responsibility flow between device blocks.

Prerequisites

Before starting, prepare the Raspberry Pi and basic software environment.

  1. Hardware prerequisites
  2. Raspberry Pi 4 Model B
  3. MicroSD card with Raspberry Pi OS Bookworm 64-bit
  4. PN532 NFC HAT
  5. DS3231 RTC module or HAT-integrated RTC exposed on I2C
  6. Stable 5 V Raspberry Pi power supply
  7. NFC card or tag for testing

  8. Software prerequisites

  9. Raspberry Pi OS Bookworm 64-bit
  10. Python 3.11
  11. Terminal access on the Pi
  12. I2C and SPI enabled in Raspberry Pi configuration

  13. Skills assumed

  14. Editing files with nano or another text editor
  15. Running commands in a shell
  16. Reading GPIO pin labels carefully

Check Python version:

python3 --version

Expected result on Bookworm should be similar to:

Python 3.11.x

Materials

Use the exact hardware model requested.

ItemExact model / requirementPurpose
Main boardRaspberry Pi 4 Model BRuns the access-control software
NFC readerPN532 NFC HATReads NFC cards/tags
Real-time clockDS3231 RTCProvides stable timestamps
PowerOfficial or good-quality 5 V supply for Raspberry Pi 4Stable operation
StorageMicroSD card with Raspberry Pi OS Bookworm 64-bitOperating system and logs
Test mediaAt least 1 authorized NFC tag and 1 unauthorized NFC tagValidation
Optional outputSmall active buzzer or LED via safe low-voltage interfacePhysical alarm indicator later

Setup/Connection

This project avoids a circuit drawing and uses text-only connection guidance.

Connection strategy

The prototype uses:
SPI for the PN532 NFC HAT
I2C for the DS3231 RTC

Many PN532 HAT boards can be configured for SPI, I2C, or UART using switches/jumpers. For this tutorial:
– Set the PN532 HAT to SPI mode
– Keep the DS3231 on I2C

Raspberry Pi interface enable steps

Run:

sudo raspi-config

Then:
1. Go to Interface Options
2. Enable SPI
3. Enable I2C
4. Finish and reboot

After reboot, verify:

ls /dev/spidev*
ls /dev/i2c-*

You should see devices similar to:
/dev/spidev0.0
/dev/i2c-1

Text-based connection notes

PN532 NFC HAT

If your PN532 HAT stacks directly onto the Raspberry Pi header, the SPI pins are already routed through the header. If using wires instead of a direct HAT stack, connect these signals:

  • PN532 VCC -> Raspberry Pi 3.3 V
  • PN532 GND -> Raspberry Pi GND
  • PN532 SCK -> Raspberry Pi SPI SCLK
  • PN532 MISO -> Raspberry Pi SPI MISO
  • PN532 MOSI -> Raspberry Pi SPI MOSI
  • PN532 SS/CS -> Raspberry Pi SPI CE0
  • PN532 RSTO or RSTPDN -> optional GPIO if required by your board, otherwise leave according to HAT design
  • PN532 mode selector -> SPI

DS3231 RTC

If the RTC is a separate module:
– DS3231 VCC -> Raspberry Pi 3.3 V
– DS3231 GND -> Raspberry Pi GND
– DS3231 SDA -> Raspberry Pi GPIO2 / SDA1
– DS3231 SCL -> Raspberry Pi GPIO3 / SCL1

Bus detection checks

Install common tools:

sudo apt update
sudo apt install -y i2c-tools python3-pip

Check I2C devices:

sudo i2cdetect -y 1

A DS3231 often appears around address 0x68.

For SPI, there is no equivalent single probe as simple as i2cdetect, but the existence of /dev/spidev0.0 confirms the SPI interface is enabled.

Project directory

Create a clean working directory:

mkdir -p ~/nfc-door-access-alarm
cd ~/nfc-door-access-alarm

Validated Code

The code below is designed to satisfy two important goals:
1. Be useful on the real Raspberry Pi with hardware adapter classes.
2. Be runnable in dry-run/mock mode on a normal computer without NFC or RTC hardware.

This matches the requested Raspberry Pi validation style.

access_controller.py

Public preview of the validated file. The complete source is shown to members and in PDF/Print.

#!/usr/bin/env python3
from __future__ import annotations

import argparse
import csv
import json
import os
import sys
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Iterable, List, Optional


@dataclass
class AccessEvent:
    timestamp: str
    uid: str
    authorized: bool
    source: str
    alarm_active: bool


class RTCAdapter:
    def now_iso(self) -> str:
        raise NotImplementedError


class SystemRTC(RTCAdapter):
    def now_iso(self) -> str:
        return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")


class MockDS3231RTC(RTCAdapter):
    def __init__(self, fixed_time: Optional[str] = None) -> None:
        self.fixed_time = fixed_time

    def now_iso(self) -> str:
        if self.fixed_time:
            return self.fixed_time
        return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")


class NFCReaderAdapter:
    def poll_uid(self) -> Optional[str]:
        raise NotImplementedError


class MockPN532Reader(NFCReaderAdapter):
    def __init__(self, sequence: Iterable[str], repeat: bool = False) -> None:
        self._sequence = list(sequence)
        self._repeat = repeat
        self._index = 0

    def poll_uid(self) -> Optional[str]:
        if not self._sequence:
            return None
        if self._index >= len(self._sequence):
            if self._repeat:
                self._index = 0
            else:
                return None
        uid = self._sequence[self._index]
        self._index += 1
        time.sleep(0.2)
        return uid


class AlarmAdapter:
    def set_alarm(self, active: bool) -> None:
        raise NotImplementedError


class ConsoleAlarm(AlarmAdapter):
    def __init__(self) -> None:
        self.state = False

    def set_alarm(self, active: bool) -> None:
        if active != self.state:
            self.state = active
            print(f"[ALARM] state={'ON' if active else 'OFF'}")


class AccessController:
    def __init__(
        self,
        rtc: RTCAdapter,
        reader: NFCReaderAdapter,
        alarm: AlarmAdapter,
        allowed_uids: List[str],
        log_path: Path,
    ) -> None:
        self.rtc = rtc
        self.reader = reader
        self.alarm = alarm
        self.allowed_uids = {uid.strip().upper() for uid in allowed_uids if uid.strip()}
        self.log_path = log_path
        self.alarm_active = False
# ...

🔒 This content is premium. With the monthly subscription (7-day free trial) you can unlock the full didactic material and the print-ready PDF pack.🔓 Unlock it — 7-day free trial
#!/usr/bin/env python3
from __future__ import annotations

import argparse
import csv
import json
import os
import sys
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Iterable, List, Optional


@dataclass
class AccessEvent:
    timestamp: str
    uid: str
    authorized: bool
    source: str
    alarm_active: bool


class RTCAdapter:
    def now_iso(self) -> str:
        raise NotImplementedError


class SystemRTC(RTCAdapter):
    def now_iso(self) -> str:
        return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")


class MockDS3231RTC(RTCAdapter):
    def __init__(self, fixed_time: Optional[str] = None) -> None:
        self.fixed_time = fixed_time

    def now_iso(self) -> str:
        if self.fixed_time:
            return self.fixed_time
        return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")


class NFCReaderAdapter:
    def poll_uid(self) -> Optional[str]:
        raise NotImplementedError


class MockPN532Reader(NFCReaderAdapter):
    def __init__(self, sequence: Iterable[str], repeat: bool = False) -> None:
        self._sequence = list(sequence)
        self._repeat = repeat
        self._index = 0

    def poll_uid(self) -> Optional[str]:
        if not self._sequence:
            return None
        if self._index >= len(self._sequence):
            if self._repeat:
                self._index = 0
            else:
                return None
        uid = self._sequence[self._index]
        self._index += 1
        time.sleep(0.2)
        return uid


class AlarmAdapter:
    def set_alarm(self, active: bool) -> None:
        raise NotImplementedError


class ConsoleAlarm(AlarmAdapter):
    def __init__(self) -> None:
        self.state = False

    def set_alarm(self, active: bool) -> None:
        if active != self.state:
            self.state = active
            print(f"[ALARM] state={'ON' if active else 'OFF'}")


class AccessController:
    def __init__(
        self,
        rtc: RTCAdapter,
        reader: NFCReaderAdapter,
        alarm: AlarmAdapter,
        allowed_uids: List[str],
        log_path: Path,
    ) -> None:
        self.rtc = rtc
        self.reader = reader
        self.alarm = alarm
        self.allowed_uids = {uid.strip().upper() for uid in allowed_uids if uid.strip()}
        self.log_path = log_path
        self.alarm_active = False

    def handle_uid(self, uid: str, source: str = "nfc") -> AccessEvent:
        normalized = uid.strip().upper()
        authorized = normalized in self.allowed_uids
        self.alarm_active = not authorized
        self.alarm.set_alarm(self.alarm_active)

        event = AccessEvent(
            timestamp=self.rtc.now_iso(),
            uid=normalized,
            authorized=authorized,
            source=source,
            alarm_active=self.alarm_active,
        )
        self._append_log(event)
        if authorized:
            print(f"{event.timestamp} ACCESS GRANTED uid={event.uid}")
        else:
            print(f"{event.timestamp} ACCESS DENIED uid={event.uid}")
        return event

    def _append_log(self, event: AccessEvent) -> None:
        file_exists = self.log_path.exists()
        with self.log_path.open("a", newline="", encoding="utf-8") as f:
            writer = csv.writer(f)
            if not file_exists:
                writer.writerow(["timestamp", "uid", "authorized", "source", "alarm_active"])
            writer.writerow([
                event.timestamp,
                event.uid,
                int(event.authorized),
                event.source,
                int(event.alarm_active),
            ])

    def run(self, max_reads: int = 0, poll_delay: float = 0.5) -> int:
        reads = 0
        while True:
            uid = self.reader.poll_uid()
            if uid:
                self.handle_uid(uid)
                reads += 1
            else:
                time.sleep(poll_delay)

            if max_reads > 0 and reads >= max_reads:
                break
        return 0


def load_allowed_uids(path: Path) -> List[str]:
    with path.open("r", encoding="utf-8") as f:
        data = json.load(f)
    if not isinstance(data, dict) or "allowed_uids" not in data:
        raise ValueError("allowlist file must contain a JSON object with key 'allowed_uids'")
    items = data["allowed_uids"]
    if not isinstance(items, list):
        raise ValueError("'allowed_uids' must be a list")
    return [str(x) for x in items]


def build_mock_sequence(args: argparse.Namespace) -> List[str]:
    if args.mock_sequence:
        return [x.strip().upper() for x in args.mock_sequence.split(",") if x.strip()]
    return [
        "04A1B2C3D4",
        "1122334455",
        "04A1B2C3D4",
    ]


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="NFC door access alarm prototype")
    parser.add_argument("--allowlist", default="allowlist.json", help="Path to JSON allowlist")
    parser.add_argument("--log", default="access_log.csv", help="Path to CSV log file")
    parser.add_argument("--mock", action="store_true", help="Use mock NFC and RTC adapters")
    parser.add_argument("--mock-sequence", default="", help="Comma-separated UID sequence for mock mode")
    parser.add_argument("--max-reads", type=int, default=3, help="Stop after this many successful reads, 0=forever")
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    allowlist_path = Path(args.allowlist)
    log_path = Path(args.log)

    allowed_uids = load_allowed_uids(allowlist_path)

    if args.mock:
        rtc: RTCAdapter = MockDS3231RTC()
        reader: NFCReaderAdapter = MockPN532Reader(build_mock_sequence(args), repeat=False)
    else:
        # real-hardware adapter integration is intentionally not auto-imported here.
        # If no mock mode is selected, use system clock and require explicit future extension
        # for PN532 hardware reading.
        rtc = SystemRTC()
        reader = MockPN532Reader([], repeat=False)

    alarm = ConsoleAlarm()
    controller = AccessController(
        rtc=rtc,
        reader=reader,
        alarm=alarm,
        allowed_uids=allowed_uids,
        log_path=log_path,
    )
    return controller.run(max_reads=args.max_reads)


if __name__ == "__main__":
    sys.exit(main())

allowlist.json

{
  "allowed_uids": [
    "04A1B2C3D4",
    "AABBCCDDEE"
  ]
}

test_access_controller.py

This validation script performs a dry-run check using mock inputs. It is not a full unit-test framework dependency; it uses only the standard library.

Public preview of the validated file. The complete source is shown to members and in PDF/Print.

#!/usr/bin/env python3
from __future__ import annotations

import csv
import subprocess
import sys
from pathlib import Path


def main() -> int:
    project_dir = Path(__file__).resolve().parent
    log_path = project_dir / "test_access_log.csv"

    if log_path.exists():
        log_path.unlink()

    cmd = [
        sys.executable,
        str(project_dir / "access_controller.py"),
        "--mock",
        "--allowlist",
        str(project_dir / "allowlist.json"),
        "--log",
        str(log_path),
        "--mock-sequence",
        "04A1B2C3D4,DEADBEEF01",
        "--max-reads",
        "2",
    ]

    result = subprocess.run(cmd, capture_output=True, text=True, check=False)
    print(result.stdout)
# ...

🔒 This content is premium. With the monthly subscription (7-day free trial) you can unlock the full didactic material and the print-ready PDF pack.🔓 Unlock it — 7-day free trial
#!/usr/bin/env python3
from __future__ import annotations

import csv
import subprocess
import sys
from pathlib import Path


def main() -> int:
    project_dir = Path(__file__).resolve().parent
    log_path = project_dir / "test_access_log.csv"

    if log_path.exists():
        log_path.unlink()

    cmd = [
        sys.executable,
        str(project_dir / "access_controller.py"),
        "--mock",
        "--allowlist",
        str(project_dir / "allowlist.json"),
        "--log",
        str(log_path),
        "--mock-sequence",
        "04A1B2C3D4,DEADBEEF01",
        "--max-reads",
        "2",
    ]

    result = subprocess.run(cmd, capture_output=True, text=True, check=False)
    print(result.stdout)
    if result.returncode != 0:
        print(result.stderr)
        return result.returncode

    if not log_path.exists():
        print("ERROR: log file was not created")
        return 1

    with log_path.open("r", encoding="utf-8", newline="") as f:
        rows = list(csv.DictReader(f))

    if len(rows) != 2:
        print(f"ERROR: expected 2 log entries, got {len(rows)}")
        return 1

    if rows[0]["authorized"] != "1":
        print("ERROR: first UID should be authorized")
        return 1

    if rows[1]["authorized"] != "0":
        print("ERROR: second UID should be unauthorized")
        return 1

    if rows[1]["alarm_active"] != "1":
        print("ERROR: alarm should be active for unauthorized UID")
        return 1

    print("Dry-run validation passed.")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Build/Flash/Run commands

This is a Raspberry Pi Python project, so there is no firmware flashing step. Instead, create files, validate syntax, and run.

1) Install package support and check imports

cd ~/nfc-door-access-alarm
python3 --version
python3 -c "import sys, csv, json, argparse, pathlib; print('standard-library-import-check: OK')"

2) Save the files

Create the main application:

nano access_controller.py

Create the allowlist:

nano allowlist.json

Create the validation script:

nano test_access_controller.py

3) Validate Python syntax

python3 -m py_compile access_controller.py test_access_controller.py

If successful, this command prints nothing.

4) Run dry-run validation on Raspberry Pi or any normal computer

python3 test_access_controller.py

Expected output will include lines similar to:

2026-... ACCESS GRANTED uid=04A1B2C3D4
[ALARM] state=ON
2026-... ACCESS DENIED uid=DEADBEEF01
Dry-run validation passed.

5) Run the main prototype manually in mock mode

python3 access_controller.py --mock --allowlist allowlist.json --log access_log.csv --mock-sequence 04A1B2C3D4,1122334455,04A1B2C3D4 --max-reads 3

6) Inspect generated access logs

cat access_log.csv

Expected structure:

timestamp,uid,authorized,source,alarm_active
2026-...,04A1B2C3D4,1,nfc,0
2026-...,1122334455,0,nfc,1
2026-...,04A1B2C3D4,1,nfc,0

Step-by-step Validation

This section validates the project around the actual goal: NFC-controlled access with alarm and timestamped logging.

1) Confirm operating system and Python version

Run:

uname -a
python3 --version

You want:
– Raspberry Pi OS Bookworm 64-bit
– Python 3.11.x

2) Confirm required buses are enabled

Run:

ls /dev/spidev*
ls /dev/i2c-*

Success criteria:
– SPI device exists for PN532 path planning
– I2C device exists for DS3231 path planning

3) Confirm RTC visibility on I2C

Run:

sudo i2cdetect -y 1

Success criteria:
– A device appears at 68 or another expected RTC address according to your hardware

What this proves:
– The DS3231 is electrically reachable on the I2C bus

What it does not yet prove:
– The application is actively reading RTC time from a dedicated hardware driver in this tutorial version

4) Confirm code syntax validity

Run:

python3 -m py_compile access_controller.py test_access_controller.py

Success criteria:
– No errors reported

This proves:
– The Python files are syntactically valid

It does not prove:
– Real PN532 transaction success

5) Validate access decision logic in mock mode

Run:

python3 access_controller.py --mock --allowlist allowlist.json --log access_log.csv --mock-sequence 04A1B2C3D4,CAFEBABE00 --max-reads 2

Success criteria:
– First event prints ACCESS GRANTED
– Second event prints ACCESS DENIED
– Console shows alarm changing to ON for unauthorized access
access_log.csv is created

6) Validate log structure

Run:

cat access_log.csv

Check for:
– Header row
– Exactly two event rows
– Authorized event marked 1
– Unauthorized event marked 0
– Unauthorized event has alarm_active equal to 1

7) Run the included automatic dry-run validator

Run:

python3 test_access_controller.py

Success criteria:
– Final line: Dry-run validation passed.

8) Real-hardware next step

In a classroom, the next practical extension is replacing MockPN532Reader with a real PN532 SPI adapter and replacing MockDS3231RTC with a DS3231 reader. The core logic, event logging, and alarm behavior remain the same, so you validate hardware in layers instead of debugging everything at once.

Troubleshooting

The Pi does not show /dev/spidev0.0

  • Re-run sudo raspi-config
  • Enable SPI again
  • Reboot
  • Check whether another overlay or configuration disabled SPI

i2cdetect does not show address 68

  • Recheck DS3231 wiring:
  • SDA to GPIO2
  • SCL to GPIO3
  • GND common
  • 3.3 V supply
  • Some modules are labeled for 5 V but may still expose I2C lines incorrectly for direct Pi use; confirm your module’s logic compatibility
  • Verify I2C is enabled

py_compile reports a syntax error

  • Reopen the file and look for:
  • Missing quotes
  • Broken indentation
  • Accidental line wrapping from copy/paste
  • Save again and rerun:
python3 -m py_compile access_controller.py test_access_controller.py

Dry-run test does not create a log file

  • Confirm you are in the correct folder
  • Check file permissions:
pwd
ls -l
  • Make sure allowlist.json exists and contains valid JSON

All tags are denied

  • Make sure the UID in allowlist.json exactly matches expected tag formatting
  • The code normalizes to uppercase without spaces, so store UIDs in uppercase for clarity

Alarm state never changes

  • In this tutorial, the alarm is a software state printed to the console
  • If you later add a buzzer or GPIO output, confirm that your hardware output code is actually calling set_alarm(True) and set_alarm(False)

Improvements

Once the basic prototype works, you can evolve it into a more realistic access unit.

Software improvements

  • Add real PN532 SPI driver integration
  • Wrap the hardware-specific code inside a PN532SPIReader adapter
  • Keep the same poll_uid() interface
  • Add real DS3231 register access
  • Implement a DS3231RTC adapter using I2C reads
  • Use BCD conversion and return ISO timestamps
  • Store user names
  • Extend allowlist.json to map UID to owner name
  • Alarm timeout
  • Instead of clearing alarm immediately on the next valid read, keep it active for a configured number of seconds
  • Tamper log
  • Count repeated failed card attempts and raise a stronger alert after three denials
  • Door release output
  • Add a transistor-driven relay module for a low-voltage lock simulator
  • Simple web status page
  • Serve the latest access state and recent logs from a local-only web interface

Physical prototype improvements

  • Put the Pi, RTC, and reader in a small enclosure
  • Mount the NFC reader near a door frame or cabinet
  • Add a clearly labeled status LED:
  • Green for granted
  • Red for denied
  • Add a low-power buzzer for denied access indication
  • Use a UPS HAT or clean power supply to improve logging reliability

Final Checklist

Use this checklist before declaring the project complete:

  • [ ] Raspberry Pi OS Bookworm 64-bit is installed on the Raspberry Pi 4 Model B
  • [ ] Python version is 3.11.x
  • [ ] SPI is enabled
  • [ ] I2C is enabled
  • [ ] PN532 NFC HAT is set to SPI mode
  • [ ] DS3231 RTC is connected on I2C
  • [ ] Project folder ~/nfc-door-access-alarm exists
  • [ ] access_controller.py is saved
  • [ ] allowlist.json is saved
  • [ ] test_access_controller.py is saved
  • [ ] python3 -m py_compile access_controller.py test_access_controller.py runs without errors
  • [ ] python3 test_access_controller.py prints Dry-run validation passed.
  • [ ] Manual mock run shows one granted and one denied event
  • [ ] access_log.csv contains timestamped records
  • [ ] You understand that this is an educational prototype, not a certified security product

With this prototype, you have a practical base for a real NFC access logger and door alarm controller using the Raspberry Pi 4 Model B + PN532 NFC HAT + DS3231 RTC.

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What Raspberry Pi model is specified for building this prototype?




Question 2: What component is used to read the NFC cards?




Question 3: What is the purpose of the DS3231 RTC in this system?




Question 4: What happens when an unauthorized tag is detected?




Question 5: What is the typical card-read handling time for this system?




Question 6: Why is the DS3231 RTC important for offline operation?




Question 7: Which of the following is a mentioned use case for this prototype?




Question 8: How does the system determine if a card is approved?




Question 9: In what formats does the system store the event audit trail?




Question 10: What data is included in the stored event logs?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:
Scroll to Top