Practical case: ESP32 Soil Moisture Monitor

Practical case: ESP32 Soil Moisture Monitor — hero

Objective and use case

What you’ll build: A continuous plant hydration monitor that measures soil moisture using an analog capacitive sensor, triggering local visual and audible alerts when watering is required.

Why it matters / Use cases

  • Agricultural prototyping: Serves as a foundational data-collection node for automated greenhouse irrigation systems, ensuring plants receive water based on objective metrics rather than schedules.
  • Hardware longevity: Demonstrates the practical advantage of capacitive sensing over legacy resistive sensors, eliminating rapid galvanic corrosion and soil contamination.
  • Preventing plant stress: Replaces guesswork with empirical, real-time soil dielectric measurements to prevent both overwatering (root rot) and underwatering.
  • Signal processing: Provides a tangible application for mapping raw, non-linear 12-bit Analog-to-Digital Converter (ADC) values into intuitive 0–100% moisture metrics.

Expected outcome

  • Continuous real-time serial output displaying both raw 12-bit ADC values (0–4095) and calculated moisture percentages (0–100%) at a ~1Hz polling rate.
  • Immediate visual alert (status LED illumination) and audible cues triggered within milliseconds of the moisture level dropping below the defined threshold.

Audience: IoT hobbyists and embedded developers; Level: Beginner to Intermediate

Architecture/flow: Capacitive Soil Sensor → Microcontroller ADC (12-bit analog read) → Moisture Percentage Mapping → GPIO Output (LED/Buzzer) & Serial Monitor

Educational validation note

Before publication, this case passed the Prometeo automated validation gate with status PASS. For this ESP32 DevKitC profile, the project was checked as a PlatformIO project: the validator extracted platformio.ini and src/main.cpp, created a temporary project and ran pio run against platform = espressif32, board = esp32dev and framework = arduino. It also checked article structure, copy/paste-safe ASCII command options, and unsupported stacks such as direct ESP-IDF or non-scoped ESP32 boards.

Published validation evidence

  • Automatic result: PASS.
  • Parsed structure: 3 sections, 2 tables and 2 code blocks detected before publication.
  • Checked code: 1 PlatformIO config + 1 ESP32 source/pio run.
  • Supported catalog: the article text was checked against Prometeo’s validation-capable device profiles, and unsupported stacks block publication.
  • Report findings: no blocking findings.

This validation confirms syntax and tool compatibility for the published code, but it does not replace physical testing on your exact ESP32 DevKitC board, wiring, power supply and local WiFi environment.

Educational safety note

This project is an educational prototype, not a certified product. Before powering the setup, verify the pinout of your exact ULX3S board revision, keep FPGA I/O signals at 3.3 V, never connect 5 V directly to I/O pins, disconnect power before changing wiring, and use suitable external supplies for loads, motors or servos while sharing ground only when the wiring requires it.

Conceptual block diagram

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

Functional architecture

ULX3S buttons

Sync/debounce

Mode selector

20 ms period generator

Pulse-width comparator

50 Hz PWM output

SG90 servo

Conceptual control flow: button input, mode selection, PWM timing and servo motion.

Validation path

Verilog source

Verilator lint/testbench

Yosys synthesis

nextpnr-ecp5

ecppack bitstream

Programmed ULX3S

The automated validation checks syntax, simulation/lint and compatibility with the ULX3S/ECP5 toolchain.

Prerequisites

To successfully complete this tutorial, students should have:
* A basic understanding of the difference between analog signals (continuous voltage levels) and digital signals (binary HIGH/LOW states).
* PlatformIO IDE installed (preferably as a Visual Studio Code extension) or the PlatformIO Core CLI.
* Familiarity with basic breadboarding techniques, including routing power rails and using current-limiting resistors for LEDs.
* Basic C/C++ programming knowledge (variables, if/else conditional logic, and functions).

Materials

You must use the following exact hardware configuration to ensure the provided code and pinouts work without modification:

  • ESP32 DevKitC + capacitive soil moisture sensor v1.2 + status LED + piezo buzzer
  • 1x 330Ω resistor (Color code: Orange-Orange-Brown, used for the status LED to prevent overcurrent).
  • 1x Standard Breadboard (830 tie-points recommended).
  • Assorted Dupont jumper wires (Male-to-Male and Male-to-Female depending on your specific sensor header).
  • 1x Micro-USB or USB-C cable (ensure it is a data-sync cable, not a charge-only cable).
  • A cup of dry soil and a cup of water (for calibration and testing).

Setup/Connection

Proper hardware connection is critical. The ESP32 operates at 3.3V logic levels. The capacitive soil moisture sensor v1.2 includes an onboard voltage regulator, but it is best practice to power it from the ESP32’s 3.3V pin to ensure its analog output never exceeds the ESP32’s maximum ADC input rating (3.3V). Supplying 5V to the sensor could result in analog signals that permanently damage the ESP32’s GPIO pins.

We use GPIO 34 for the analog input. The ESP32 has two internal ADCs. ADC2 is shared with the Wi-Fi radio and cannot be used reliably when Wi-Fi is active. ADC1 (which includes GPIO 34) functions independently of the Wi-Fi stack, making it the standard choice for robust sensor readings.

Wiring Table

Component Component Pin / Lead ESP32 DevKitC Pin Notes
Capacitive Sensor VCC / V+ 3V3 Power via 3.3V to protect ESP32 ADC.
Capacitive Sensor GND / G GND Common ground reference.
Capacitive Sensor AOUT / AU GPIO 34 Connected to ADC1_CH6.
Status LED Anode (Long Leg) GPIO 25 Connect via the 330Ω resistor.
Status LED Cathode (Short Leg) GND Common ground reference.
Piezo Buzzer Positive (+) GPIO 26 Driven via ESP32 hardware PWM (LEDC).
Piezo Buzzer Negative (-) GND Common ground reference.

Hardware Note: The piezo buzzer can be active or passive. The code provided uses a Pulse Width Modulation (PWM) signal, which will generate a specific tone on a passive buzzer, and will also successfully trigger an active buzzer by toggling its power rapidly.

Validated Code

The project uses PlatformIO. You will need to configure your environment file and your main application source file.

platformio.ini

Create or replace the contents of your platformio.ini file with the following configuration:

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200

src/main.cpp

Copy the following C++ code into your src/main.cpp file. This code implements the continuous analog reading, performs linear interpolation to calculate the moisture percentage, and handles the logic for the hardware alerts.

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

#include <Arduino.h>

// --------------------------------------------------------
// Hardware Pin Definitions
// --------------------------------------------------------
const int MOISTURE_PIN = 34; // ADC1 Channel 6
const int LED_PIN      = 25; // Digital Output for Status LED
const int BUZZER_PIN   = 26; // Digital Output for Piezo Buzzer

// --------------------------------------------------------
// PWM Configuration for Piezo Buzzer
// --------------------------------------------------------
const int PWM_FREQ       = 2000; // 2 kHz audio frequency
const int PWM_CHANNEL    = 0;    // ESP32 Hardware Timer Channel 0
const int PWM_RESOLUTION = 8;    // 8-bit resolution (0-255)

// --------------------------------------------------------
// Calibration Constants
// --------------------------------------------------------
// IMPORTANT: Capacitive sensors output a HIGHER voltage when dry 
// and a LOWER voltage when wet. These values represent the 12-bit 
// ADC readings (0-4095). You must calibrate these for your specific sensor.
const int DRY_VALUE = 3200;  // Expected ADC reading in completely dry air
const int WET_VALUE = 1400;  // Expected ADC reading submerged in water

// --------------------------------------------------------
// Application Logic Constants
// --------------------------------------------------------
const int ALARM_THRESHOLD_PERCENT = 30; // Trigger alert at or below 30% moisture

void setup() {
    // Initialize Serial Communication for debugging
    Serial.begin(115200);

    // Configure Digital Output Pins
    pinMode(LED_PIN, OUTPUT);
    digitalWrite(LED_PIN, LOW); // Ensure LED is off at boot

    // Configure LEDC PWM peripheral for the Buzzer
    // This allows us to generate a clean square wave for a passive piezo
    ledcSetup(PWM_CHANNEL, PWM_FREQ, PWM_RESOLUTION);
    ledcAttachPin(BUZZER_PIN, PWM_CHANNEL);
    ledcWrite(PWM_CHANNEL, 0); // Ensure buzzer is silent at boot

    Serial.println("=========================================");
    Serial.println(" ESP32 Soil Moisture Monitor Initialized ");
    Serial.println("=========================================");

    // Allow hardware to stabilize
    delay(1000); 
}
// ... continues for members in the complete validated source ...

🔒 Part of the validated code is premium. With the 7-day pass or the monthly membership you can view the complete validated source.

#include <Arduino.h>

// --------------------------------------------------------
// Hardware Pin Definitions
// --------------------------------------------------------
const int MOISTURE_PIN = 34; // ADC1 Channel 6
const int LED_PIN      = 25; // Digital Output for Status LED
const int BUZZER_PIN   = 26; // Digital Output for Piezo Buzzer

// --------------------------------------------------------
// PWM Configuration for Piezo Buzzer
// --------------------------------------------------------
const int PWM_FREQ       = 2000; // 2 kHz audio frequency
const int PWM_CHANNEL    = 0;    // ESP32 Hardware Timer Channel 0
const int PWM_RESOLUTION = 8;    // 8-bit resolution (0-255)

// --------------------------------------------------------
// Calibration Constants
// --------------------------------------------------------
// IMPORTANT: Capacitive sensors output a HIGHER voltage when dry 
// and a LOWER voltage when wet. These values represent the 12-bit 
// ADC readings (0-4095). You must calibrate these for your specific sensor.
const int DRY_VALUE = 3200;  // Expected ADC reading in completely dry air
const int WET_VALUE = 1400;  // Expected ADC reading submerged in water

// --------------------------------------------------------
// Application Logic Constants
// --------------------------------------------------------
const int ALARM_THRESHOLD_PERCENT = 30; // Trigger alert at or below 30% moisture

void setup() {
    // Initialize Serial Communication for debugging
    Serial.begin(115200);

    // Configure Digital Output Pins
    pinMode(LED_PIN, OUTPUT);
    digitalWrite(LED_PIN, LOW); // Ensure LED is off at boot

    // Configure LEDC PWM peripheral for the Buzzer
    // This allows us to generate a clean square wave for a passive piezo
    ledcSetup(PWM_CHANNEL, PWM_FREQ, PWM_RESOLUTION);
    ledcAttachPin(BUZZER_PIN, PWM_CHANNEL);
    ledcWrite(PWM_CHANNEL, 0); // Ensure buzzer is silent at boot

    Serial.println("=========================================");
    Serial.println(" ESP32 Soil Moisture Monitor Initialized ");
    Serial.println("=========================================");

    // Allow hardware to stabilize
    delay(1000); 
}

void loop() {
    // 1. Read the raw analog voltage from the sensor
    int rawAdcValue = analogRead(MOISTURE_PIN);

    // 2. Map the raw ADC value to a 0-100 percentage scale.
    // We invert the mapping parameters because DRY is high, WET is low.
    int moisturePercent = map(rawAdcValue, DRY_VALUE, WET_VALUE, 0, 100);

    // 3. Constrain the percentage to strictly 0-100.
    // This prevents negative numbers or values > 100% if the sensor 
    // reads slightly outside the hardcoded calibration range.
    moisturePercent = constrain(moisturePercent, 0, 100);

    // 4. Output telemetry to the Serial Monitor
    Serial.print("Raw ADC: ");
    Serial.print(rawAdcValue);
    Serial.print("\t | Moisture: ");
    Serial.print(moisturePercent);
    Serial.println("%");

    // 5. Evaluate the alert threshold
    if (moisturePercent <= ALARM_THRESHOLD_PERCENT) {
        // Condition: Soil is dangerously dry.
        // Action: Flash the LED and pulse the buzzer.

        digitalWrite(LED_PIN, HIGH);
        // Set PWM duty cycle to 50% (128 out of 255) to generate sound
        ledcWrite(PWM_CHANNEL, 128); 
        delay(500); // Wait 500ms

        digitalWrite(LED_PIN, LOW);
        // Set PWM duty cycle to 0% to silence buzzer
        ledcWrite(PWM_CHANNEL, 0);   
        delay(500); // Wait 500ms

    } else {
        // Condition: Soil moisture is adequate.
        // Action: Keep alerts turned off and delay before next reading.

        digitalWrite(LED_PIN, LOW);
        ledcWrite(PWM_CHANNEL, 0);
        delay(1000); // Poll sensor once per second
    }
}

Build/Flash/Run commands

To compile, upload, and monitor the code on your ESP32 DevKitC, use the PlatformIO CLI. Open your terminal in the root directory of your project (where platformio.ini is located) and execute the following commands.

Command Reference

Action Command Purpose
Build pio run Compiles the C++ source code and links the Arduino framework.
Upload pio run --target upload Flashes the compiled firmware binary to the ESP32.
Monitor pio device monitor Opens the serial monitor to view the telemetry output.

Execution Workflow

  1. Connect the ESP32 DevKitC to your computer via the USB cable. Ensure the CP210x or CH34x drivers are installed if your OS does not recognize the device automatically.
  2. Run pio run to verify that there are no syntax errors and that the environment is configured correctly.
  3. Run pio run --target upload to flash the device. If the terminal displays “Connecting…” and stalls, you may need to press and hold the “BOOT” button on the ESP32 DevKitC until the flashing process begins.
  4. Run pio device monitor to observe the serial output. You should immediately see the initialization banner followed by continuous ADC readings.

Step-by-step Validation

Follow these checkpoints to ensure your prototype is fully functional and correctly calibrated.

  1. Serial Communication Check:
    • Action: Open the serial monitor after flashing.
    • Expected Observation: The console prints “ESP32 Soil Moisture Monitor Initialized” followed by data lines every second.
    • Pass Condition: Text is legible (no garbled characters), confirming the 115200 baud rate is correct.
  2. Dry Air Baseline (0% Calibration):
    • Action: Hold the sensor in the open air, touching nothing.
    • Expected Observation: The Raw ADC value stabilizes (e.g., around 3100 to 3300).
    • Pass Condition: Note this number. Update the DRY_VALUE constant in the code to match this reading and re-flash the ESP32 if it deviates by more than 100 from the default.
  3. Water Submersion (100% Calibration):
    • Action: Submerge the capacitive sensor in a glass of water up to the marked safety line.
    • Expected Observation: The Raw ADC value drops significantly and stabilizes (e.g., around 1400).
    • Pass Condition: The serial monitor outputs 100%. Update the WET_VALUE constant in the code and re-flash if it does not reach 100%.
  4. Alert Logic Verification:
    • Action: Slowly remove the sensor from the water and wipe it completely dry.
    • Expected Observation: The moisture percentage drops. Once it hits 30% or lower, the LED flashes and the buzzer emits a 2000 Hz tone.
    • Pass Condition: Visual and audible alerts trigger synchronously exactly when the moisture reads <= 30%, verifying the logic threshold is functioning safely.

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 type of sensor is used to measure soil moisture in this project?




Question 2: Why is capacitive sensing preferred over legacy resistive sensors in this project?




Question 3: What is the resolution of the Analog-to-Digital Converter (ADC) mentioned in the text?




Question 4: What is the range of the raw ADC values before they are mapped to moisture percentages?




Question 5: What is the polling rate for the real-time serial output?




Question 6: What happens when the moisture level drops below the defined threshold?




Question 7: What agricultural issue does empirical, real-time soil dielectric measurement help prevent?




Question 8: What does the project map the raw 12-bit ADC values into?




Question 9: What type of measurements replace guesswork to prevent plant stress?




Question 10: What is one of the use cases for this plant hydration monitor?




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

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

Follow me:


Practical case: ESP32 Local Network Monitor

Practical case: ESP32 Local Network Monitor — hero

Objective and use case

What you’ll build: A standalone Field Technician’s Local Network Monitor using an ESP32 configured as a Software Access Point (SoftAP). It broadcasts a secure, localized Wi-Fi network to serve a real-time, air-gapped diagnostic dashboard directly to a smartphone or tablet.

Why it matters / Use cases

  • Infrastructure-independent diagnostics: Allows technicians to connect directly to equipment in offline environments like remote agricultural fields, deep industrial basements, or new construction sites.
  • Safe machine fault logging: Interfaces with industrial fault relays so personnel can safely monitor machine states via browser, avoiding physical exposure to high-voltage control panels.
  • Isolated human-machine interface (HMI): Delivers a highly secure, air-gapped configuration portal that cannot be accessed from the public internet, drastically reducing cybersecurity attack surfaces.
  • Access control monitoring: Acts as a localized, temporary monitor for server rack doors or secure gates, logging open/close states instantly.

Expected outcome

  • The ESP32 reliably broadcasts a WPA2-secured Wi-Fi network (SSID: ESP32-FieldMonitor) with a client connection time of <2 seconds.
  • A fully mobile-responsive web dashboard loads without external internet, rendering diagnostic UI elements.
  • Real-time hardware status and fault logs update on the client browser with <50ms network latency.

Audience: Industrial IoT Developers, Field Technicians, Maintenance Engineers; Level: Intermediate

Architecture/flow: ESP32 (SoftAP Mode) → Broadcasts WPA2 SSID → Technician Smartphone Connects → ESP32 Web Server → Serves HTML/JS Dashboard & Streams Real-time GPIO Data.

Educational validation note

Before publication, this case passed the Prometeo automated validation gate with status PASS. For this ESP32 DevKitC profile, the project was checked as a PlatformIO project: the validator extracted platformio.ini and src/main.cpp, created a temporary project and ran pio run against platform = espressif32, board = esp32dev and framework = arduino. It also checked article structure, copy/paste-safe ASCII command options, and unsupported stacks such as direct ESP-IDF or non-scoped ESP32 boards.

Published validation evidence

  • Automatic result: PASS.
  • Parsed structure: 4 sections, 2 tables and 2 code blocks detected before publication.
  • Checked code: 1 PlatformIO config + 1 ESP32 source/pio run.
  • Supported catalog: the article text was checked against Prometeo’s validation-capable device profiles, and unsupported stacks block publication.
  • Report findings: no blocking findings.

This validation confirms syntax and tool compatibility for the published code, but it does not replace physical testing on your exact ESP32 DevKitC board, wiring, power supply and local WiFi environment.

Educational safety note

This project is an educational prototype, not a certified product. Before powering the setup, verify the pinout of your exact ULX3S board revision, keep FPGA I/O signals at 3.3 V, never connect 5 V directly to I/O pins, disconnect power before changing wiring, and use suitable external supplies for loads, motors or servos while sharing ground only when the wiring requires it.

Conceptual block diagram

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

Functional architecture

ESP32 (SoftAP Mode)

Broadcasts WPA2 SSID

Technician Smartphone Connects

ESP32 Web Server

Serves HTML/JS Dashboard & Streams Real-t…

Conceptual signal and responsibility flow between device blocks.

Validation path

Source code

PlatformIO build

Flash

Serial monitor

Conceptual summary of the tools used to check the published material.

Prerequisites

To successfully complete this tutorial, you will need:
* Basic understanding of C++ programming and microcontroller GPIO concepts.
* PlatformIO IDE installed (either as a Visual Studio Code extension or via the Command Line Interface).
* Basic knowledge of networking concepts, specifically Wi-Fi Access Points (SSID, WPA2) and IP addressing.
* A web browser on a Wi-Fi enabled device (smartphone, tablet, or laptop) to view the dashboard.


Materials

  • Microcontroller: ESP32 DevKitC (Exact model: ESP32 DevKitC V4, typically equipped with the ESP32-WROOM-32 module).
  • Input Device: Pushbutton or dry contact switch (this will simulate an external machine fault relay or door contact).
  • Visual Indicator: Standard 5mm Status LED (e.g., Blue or Green).
  • Passive Components: 1x 330Ω resistor (current limiting for the LED).
  • Prototyping: Standard standard breadboard and male-to-male jumper wires.
  • Connection: Micro-USB cable (must support both power and data transfer; charging-only cables will fail to flash the device).

(Note: Depending on your specific ESP32 DevKitC manufacturer, you may need to install CP210x or CH34x USB-to-UART drivers on your operating system to allow PlatformIO to recognize the device).


Setup/Connection

The hardware setup utilizes the ESP32’s internal pull-up resistors for the contact input, minimizing the need for external passive components. When the contact switch is open, the internal resistor pulls the pin HIGH. When the switch is closed, it connects the pin to Ground (LOW).

Wiring Table:

Component ESP32 DevKitC Pin Connection / Destination Description
Contact Switch GPIO 18 Terminal 1 of Switch Configured as INPUT_PULLUP.
Contact Switch GND Terminal 2 of Switch Pulls GPIO 18 LOW when closed.
Status LED GPIO 19 LED Anode (Long leg) Configured as OUTPUT.
Status LED GND LED Cathode (Short leg) via 330Ω Resistor Completes the LED circuit safely.

Important Hardware Notes:
1. Ensure you are using a 330Ω resistor in series with the Status LED to prevent drawing excessive current from the ESP32’s GPIO pin, which could damage the microcontroller.
2. Do not connect the contact switch to any external voltage source. It must act as a “dry contact” (a simple mechanical closure to Ground).


Validated Code

The project requires two files within your PlatformIO workspace. The platformio.ini file configures the build environment, while src/main.cpp contains the application logic.

PlatformIO Configuration

Create or overwrite the platformio.ini file in the root of your project directory with the following configuration. This ensures the correct board definition and serial monitor baud rate are used.

; platformio.ini
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200

Application Source Code

Create or overwrite the src/main.cpp file with the following complete, compilable code.

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

// src/main.cpp
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>

// Hardware Pin Definitions
#define CONTACT_PIN 18
#define STATUS_LED_PIN 19

// SoftAP Network Credentials
const char* ssid = "ESP32-FieldMonitor";
const char* password = "admin1234"; // WPA2 requires a minimum of 8 characters

// Initialize the WebServer on port 80
WebServer server(80);

// Global state variables
int currentContactState = HIGH;
int lastContactState = HIGH;

// HTML Dashboard stored in Program Memory (PROGMEM) to save RAM
const char dashboard_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Field Monitor Dashboard</title>
  <style>
    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      text-align: center;
      background-color: #e9ecef;
      margin: 0;
      padding: 20px;
    }
    .card {
      background: white;
      max-width: 400px;
      margin: 40px auto;
      padding: 30px;
      border-radius: 12px;
      box-shadow: 0 8px 16px rgba(0,0,0,0.1);
    }
    h2 {
      color: #343a40;
      margin-top: 0;
    }
    .status-box {
      font-size: 1.8em;
      font-weight: bold;
      margin: 20px 0;
      padding: 20px;
      border-radius: 8px;
      transition: background-color 0.3s, color 0.3s;
    }
    .loading { background-color: #f8f9fa; color: #6c757d; border: 2px dashed #6c757d; }
    .closed { background-color: #d4edda; color: #155724; border: 2px solid #28a745; }
    .open { background-color: #f8d7da; color: #721c24; border: 2px solid #dc3545; }
    .footer {
      margin-top: 20px;
      font-size: 0.85em;
      color: #6c757d;
    }
  </style>
</head>
<body>
  <div class="card">
    <h2>Machine Contact Status</h2>
    <div id="contact-state" class="status-box loading">Awaiting Data...</div>
    <div class="footer">Auto-refreshing every 500ms via JSON API</div>
  </div>

  <script>
    // Asynchronous function to fetch status from the ESP32
    function fetchStatus() {
      fetch('/api/status')
        .then(response => {
          if (!response.ok) {
            throw new Error('Network response was not ok');
          }
          return response.json();
        })
        .then(data => {
          const statusDiv = document.getElementById('contact-state');
          // data.contact is 0 when closed (LOW) due to INPUT_PULLUP
          if(data.contact === 0) {
            statusDiv.innerHTML = "CONTACT CLOSED";
            statusDiv.className = "status-box closed";
          } else {
            statusDiv.innerHTML = "CONTACT OPEN";
            statusDiv.className = "status-box open";
          }
// ... continues for members in the complete validated source ...

🔒 Part of the validated code is premium. With the 7-day pass or the monthly membership you can view the complete validated source.

// src/main.cpp
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>

// Hardware Pin Definitions
#define CONTACT_PIN 18
#define STATUS_LED_PIN 19

// SoftAP Network Credentials
const char* ssid = "ESP32-FieldMonitor";
const char* password = "admin1234"; // WPA2 requires a minimum of 8 characters

// Initialize the WebServer on port 80
WebServer server(80);

// Global state variables
int currentContactState = HIGH;
int lastContactState = HIGH;

// HTML Dashboard stored in Program Memory (PROGMEM) to save RAM
const char dashboard_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Field Monitor Dashboard</title>
  <style>
    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      text-align: center;
      background-color: #e9ecef;
      margin: 0;
      padding: 20px;
    }
    .card {
      background: white;
      max-width: 400px;
      margin: 40px auto;
      padding: 30px;
      border-radius: 12px;
      box-shadow: 0 8px 16px rgba(0,0,0,0.1);
    }
    h2 {
      color: #343a40;
      margin-top: 0;
    }
    .status-box {
      font-size: 1.8em;
      font-weight: bold;
      margin: 20px 0;
      padding: 20px;
      border-radius: 8px;
      transition: background-color 0.3s, color 0.3s;
    }
    .loading { background-color: #f8f9fa; color: #6c757d; border: 2px dashed #6c757d; }
    .closed { background-color: #d4edda; color: #155724; border: 2px solid #28a745; }
    .open { background-color: #f8d7da; color: #721c24; border: 2px solid #dc3545; }
    .footer {
      margin-top: 20px;
      font-size: 0.85em;
      color: #6c757d;
    }
  </style>
</head>
<body>
  <div class="card">
    <h2>Machine Contact Status</h2>
    <div id="contact-state" class="status-box loading">Awaiting Data...</div>
    <div class="footer">Auto-refreshing every 500ms via JSON API</div>
  </div>

  <script>
    // Asynchronous function to fetch status from the ESP32
    function fetchStatus() {
      fetch('/api/status')
        .then(response => {
          if (!response.ok) {
            throw new Error('Network response was not ok');
          }
          return response.json();
        })
        .then(data => {
          const statusDiv = document.getElementById('contact-state');
          // data.contact is 0 when closed (LOW) due to INPUT_PULLUP
          if(data.contact === 0) {
            statusDiv.innerHTML = "CONTACT CLOSED";
            statusDiv.className = "status-box closed";
          } else {
            statusDiv.innerHTML = "CONTACT OPEN";
            statusDiv.className = "status-box open";
          }
        })
        .catch(error => {
          console.error('Error fetching status:', error);
          const statusDiv = document.getElementById('contact-state');
          statusDiv.innerHTML = "CONNECTION LOST";
          statusDiv.className = "status-box loading";
        });
    }

    // Poll the API every 500 milliseconds
    setInterval(fetchStatus, 500);

    // Initial fetch immediately on load
    window.onload = fetchStatus;
  </script>
</body>
</html>
)rawliteral";

// Route Handler: Serve the main HTML dashboard
void handleRoot() {
  server.send(200, "text/html", dashboard_html);
  Serial.println("Dashboard accessed by a client.");
}

// Route Handler: Serve the JSON API for the dashboard to consume
void handleApiStatus() {
  // Construct a simple JSON string manually
  String jsonPayload = "{\"contact\": " + String(currentContactState) + "}";
  server.send(200, "application/json", jsonPayload);
}

// Route Handler: Handle 404 Not Found
void handleNotFound() {
  server.send(404, "text/plain", "404: Not Found");
}

void setup() {
  // Initialize Serial Monitor
  Serial.begin(115200);
  delay(1000); // Allow serial to stabilize
  Serial.println("\n--- ESP32 Field Monitor Initialization ---");

  // Configure Hardware Pins
  pinMode(CONTACT_PIN, INPUT_PULLUP);
  pinMode(STATUS_LED_PIN, OUTPUT);

  // Read initial state
  currentContactState = digitalRead(CONTACT_PIN);
  lastContactState = currentContactState;

  // Set initial LED state (ON when contact is closed/LOW)
  digitalWrite(STATUS_LED_PIN, (currentContactState == LOW) ? HIGH : LOW);

  // Configure Wi-Fi in Access Point (SoftAP) mode
  Serial.print("Configuring Access Point...");
  WiFi.softAP(ssid, password);

  IPAddress IP = WiFi.softAPIP();
  Serial.println(" Ready!");
  Serial.print("SoftAP SSID: ");
  Serial.println(ssid);
  Serial.print("SoftAP IP Address: ");
  Serial.println(IP);

  // Define Web Server Routing
  server.on("/", HTTP_GET, handleRoot);
  server.on("/api/status", HTTP_GET, handleApiStatus);
  server.onNotFound(handleNotFound);

  // Start the Web Server
  server.begin();
  Serial.println("HTTP Web Server started.");
}

void loop() {
  // Handle incoming HTTP client requests
  server.handleClient();

  // Read the physical contact state
  currentContactState = digitalRead(CONTACT_PIN);

  // Detect state changes to update the LED and log to Serial
  if (currentContactState != lastContactState) {
    // Debounce delay (basic implementation)
    delay(50);
    currentContactState = digitalRead(CONTACT_PIN);

    if (currentContactState != lastContactState) {
      if (currentContactState == LOW) {
        Serial.println("EVENT: Contact CLOSED (Active).");
        digitalWrite(STATUS_LED_PIN, HIGH); // Turn LED ON
      } else {
        Serial.println("EVENT: Contact OPEN (Inactive).");
        digitalWrite(STATUS_LED_PIN, LOW);  // Turn LED OFF
      }
      lastContactState = currentContactState;
    }
  }
}


Build/Flash/Run commands

Use the PlatformIO Core CLI to compile, upload, and monitor your project. Open your terminal in the project’s root directory (where platformio.ini is located) and execute the following commands.

Command Purpose
pio run Compiles the project and verifies all dependencies and syntax.
pio run --target upload Compiles and flashes the compiled firmware to the ESP32 DevKitC.
pio device monitor Opens the serial monitor to view runtime logs at 115200 baud.

Execution Workflow:
1. Connect the ESP32 DevKitC to your computer via the micro-USB cable.
2. Run pio run to ensure the code compiles without syntax errors.
3. Run pio run --target upload. (If the upload fails to connect, press and hold the BOOT button on the ESP32 DevKitC when you see “Connecting…” in the terminal).
4. Run pio device monitor to observe the initialization sequence and verify the SoftAP IP address.


Step-by-step Validation

Follow these checkpoints to ensure the prototype operates exactly as intended.

  1. Verify Serial Initialization
    • Action: Observe the terminal output immediately after running pio device monitor or pressing the EN (Reset) button on the ESP32.
    • Expected observation: The terminal prints “— ESP32 Field Monitor Initialization —“, followed by the SSID ESP32-FieldMonitor and the IP 192.168.4.1.
    • Pass condition: The ESP32 does not crash or enter a reboot loop.
  2. Verify SoftAP Broadcast
    • Action: Open the Wi-Fi settings on a smartphone or laptop.
    • Expected observation: A network named ESP32-FieldMonitor appears in the list of available networks.
    • Pass condition: You can successfully connect to the network using the password admin1234.
  3. Verify Web Dashboard Loading
    • Action: Open a web browser on the connected device and navigate to http://192.168.4.1.
    • Expected observation: The “Field Monitor Dashboard” loads, showing a styled card. The serial monitor logs “Dashboard accessed by a client.”
    • Pass condition: The UI renders correctly without broken CSS.
  4. Verify Physical Input and LED Output
    • *Action

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 is the primary hardware component used to build the Local Network Monitor?




Question 2: How is the ESP32 configured to broadcast a localized Wi-Fi network?




Question 3: Why is the diagnostic dashboard considered 'air-gapped'?




Question 4: What is one major safety benefit of using this device for machine fault logging?




Question 5: Which of the following is a mentioned use case for the Local Network Monitor?




Question 6: What type of environments is the infrastructure-independent diagnostic feature designed for?




Question 7: How does the device help reduce cybersecurity attack surfaces?




Question 8: Which security protocol is used for the broadcasted Wi-Fi network?




Question 9: What device is typically used to view the real-time diagnostic dashboard?




Question 10: What can the device log instantly when acting as an access control monitor?




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

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

Follow me:


Practical case: Ultrasonic Distance Alarm with Arduino UNO

Practical case: Ultrasonic Distance Alarm with Arduino UNO — hero

Objective and use case

What you’ll build: You will build an educational ultrasonic parking distance alarm prototype that measures vehicle proximity and provides real-time visual and acoustic feedback. The system uses non-blocking logic to simultaneously calculate distance, update an LCD, and modulate a buzzer’s beep frequency based on proximity.

Why it matters / Use cases

  • Residential Garage Parking: Helps visualize distance from a wall to prevent bumper damage (e.g., alerting precisely at 30 cm).
  • ADAS Foundation: Demonstrates the core principles behind commercial automotive parking sensors.
  • Non-blocking Logic: Replaces rudimentary delay() calls with millis()-based state machines, allowing concurrent sensor reads and display updates at 10+ Hz without freezing.

Expected outcome

  • A functional proximity alarm with < 50ms latency between object detection and buzzer response.
  • An LCD interface displaying real-time distance metrics (cm) and dynamic status warnings.
  • A passive buzzer that scales its pulse frequency dynamically, culminating in a solid continuous tone at < 10 cm.

Audience: Embedded systems learners and automotive electronics hobbyists; Level: Intermediate

Architecture/flow: Ultrasonic Sensor (Trigger/Echo) → Microcontroller (Non-blocking state machine) → I2C LCD Display & PWM Passive Buzzer

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: 3 sections, 3 tables and 2 code blocks detected before publication.
  • Checked code: 1 Arduino/arduino-cli compile, 1 Bash/copy-paste checks.
  • Supported catalog: the article text was checked against Prometeo’s validation-capable device profiles, and 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 prototype, not a certified product. Before powering the setup, verify the pinout of your exact ULX3S board revision, keep FPGA I/O signals at 3.3 V, never connect 5 V directly to I/O pins, disconnect power before changing wiring, and use suitable external supplies for loads, motors or servos while sharing ground only when the wiring requires it.

Conceptual block diagram

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

Functional architecture

ULX3S buttons

Sync/debounce

Mode selector

20 ms period generator

Pulse-width comparator

50 Hz PWM output

SG90 servo

Conceptual control flow: button input, mode selection, PWM timing and servo motion.

Validation path

Verilog source

Verilator lint/testbench

Yosys synthesis

nextpnr-ecp5

ecppack bitstream

Programmed ULX3S

The automated validation checks syntax, simulation/lint and compatibility with the ULX3S/ECP5 toolchain.

Prerequisites

To successfully complete this tutorial, you should have:
* A basic understanding of C++ syntax (variables, if/else statements, functions).
* Familiarity with breadboard prototyping and jumper wire connections.
* A computer running Windows, macOS, or Linux with a command-line interface.
* The arduino-cli (Arduino Command Line Interface) installed and added to your system’s PATH.

Materials

You will need the exact components listed below to ensure compatibility with the provided code and wiring instructions:
* Microcontroller: 1x Arduino UNO R3 (ATmega328P).
* Sensor: 1x HC-SR04 ultrasonic sensor.
* Display: 1x 16×2 HD44780 LCD (standard parallel interface, without I2C backpack).
* Audio: 1x Passive buzzer (requires a frequency signal to generate sound).
* Components: 1x 10kΩ potentiometer (for LCD contrast adjustment), 1x 220Ω resistor (for LCD backlight current limiting).
* Prototyping: 1x Standard breadboard and a set of male-to-male jumper wires.
* Power/Data: 1x USB Type-A to Type-B cable.

Setup/Connection

The HD44780 LCD uses a 4-bit parallel interface to save digital pins on the Arduino. The HC-SR04 requires one pin to trigger the ultrasonic pulse and another to read the returning echo.

Power Distribution

  1. Connect the Arduino 5V pin to the breadboard’s positive power rail.
  2. Connect the Arduino GND pin to the breadboard’s ground rail.

HC-SR04 Ultrasonic Sensor Connections

HC-SR04 Pin Arduino UNO R3 Pin Description
VCC 5V (Power rail) 5V power supply
Trig Digital Pin 9 Outputs the 10µs trigger pulse
Echo Digital Pin 10 Receives the returning pulse width
GND GND (Ground rail) Ground connection

Passive Buzzer Connections

Buzzer Pin Arduino UNO R3 Pin Description
Positive (+) Digital Pin 8 PWM/Frequency output via tone()
Negative (-) GND (Ground rail) Ground connection

16×2 HD44780 LCD Connections

Note: The 10kΩ potentiometer’s outer legs connect to 5V and GND. The center wiper connects to the LCD’s V0 pin to adjust the text contrast.

LCD Pin Name Arduino / Component Connection Description
1 VSS GND (Ground rail) Ground
2 VDD 5V (Power rail) 5V logic power
3 V0 Potentiometer center wiper Contrast adjustment
4 RS Digital Pin 12 Register Select
5 RW GND (Ground rail) Read/Write (tied low for write-only)
6 E Digital Pin 11 Enable pin
11 D4 Digital Pin 5 Data line 4
12 D5 Digital Pin 4 Data line 5
13 D6 Digital Pin 3 Data line 6
14 D7 Digital Pin 2 Data line 7
15 A 5V via 220Ω Resistor Backlight Anode (+)
16 K GND (Ground rail) Backlight Cathode (-)

Validated Code

Create a new directory named ParkingAlarm and save the following code as ParkingAlarm.ino. The pin definitions and thresholds are included at the top of the file for easy tuning.

#include <LiquidCrystal.h>

// -----------------------------------------
// Pin Definitions
// -----------------------------------------
#define TRIG_PIN 9
#define ECHO_PIN 10
#define BUZZER_PIN 8

// HD44780 LCD (4-bit mode)
#define LCD_RS 12
#define LCD_EN 11
#define LCD_D4 5
#define LCD_D5 4
#define LCD_D6 3
#define LCD_D7 2

// -----------------------------------------
// System Thresholds (in centimeters)
// -----------------------------------------
#define DIST_MAX   200  // Maximum reliable distance to process
#define DIST_WARN  100  // Start warning (slow beep)
#define DIST_ALARM 50   // Start alarm (dynamic fast beep)
#define DIST_STOP  10   // Stop immediately (continuous tone)

// Initialize the LCD library with the interface pins
LiquidCrystal lcd(LCD_RS, LCD_EN, LCD_D4, LCD_D5, LCD_D6, LCD_D7);

// Variables for non-blocking buzzer timing
unsigned long previousBuzzerMillis = 0;
bool buzzerState = false;

void setup() {
    // Initialize Serial for debugging
    Serial.begin(115200);

    // Configure hardware pins
    pinMode(TRIG_PIN, OUTPUT);
    pinMode(ECHO_PIN, INPUT);
    pinMode(BUZZER_PIN, OUTPUT);

    // Initialize the LCD's columns and rows
    lcd.begin(16, 2);

    // Display a startup message
    lcd.setCursor(0, 0);
    lcd.print("Parking Assist");
    lcd.setCursor(0, 1);
    lcd.print("System Booting..");
    delay(2000); // 2-second boot delay is acceptable here
    lcd.clear();
}

// Function to trigger the HC-SR04 and calculate distance
long readDistance() {
    // Ensure the trigger pin is low before pulsing
    digitalWrite(TRIG_PIN, LOW);
    delayMicroseconds(2);

    // Send a 10 microsecond high pulse to trigger the sensor
    digitalWrite(TRIG_PIN, HIGH);
    delayMicroseconds(10);
    digitalWrite(TRIG_PIN, LOW);

    // Read the echo pin; timeout after 30,000 microseconds (approx 5 meters)
    long duration = pulseIn(ECHO_PIN, HIGH, 30000);

    // If timeout occurred, return -1 to indicate out of range
    if (duration == 0) {
        return -1;
    }

    // Calculate distance in centimeters
    // Speed of sound is 343 m/s or 0.0343 cm/microsecond
    // Divide by 2 to account for the round trip (ping and echo)
    return (duration * 0.0343) / 2;
}

void loop() {
    long distance = readDistance();
    unsigned long currentMillis = millis();

    // Debug output
    Serial.print("Distance: ");
    Serial.println(distance);

    // Top row: Display distance
    lcd.setCursor(0, 0);
    if (distance == -1 || distance > DIST_MAX) {
        lcd.print("Out of Range    "); // Pad with spaces to clear old chars
        noTone(BUZZER_PIN);

        lcd.setCursor(0, 1);
        lcd.print("Status: STANDBY ");
    } else {
        lcd.print("Dist: ");
        if (distance < 100) lcd.print(" "); // Alignment padding
        if (distance < 10)  lcd.print(" "); // Alignment padding
        lcd.print(distance);
        lcd.print(" cm      ");

        // Bottom row: Display status and handle buzzer logic
        lcd.setCursor(0, 1);

        if (distance > DIST_WARN) {
            // Safe zone
            lcd.print("Status: SAFE    ");
            noTone(BUZZER_PIN);

        } else if (distance > DIST_ALARM) {
            // Warning zone: Slow beep
            lcd.print("Status: SLOW    ");
            if (currentMillis - previousBuzzerMillis >= 500) { // 500ms interval
                previousBuzzerMillis = currentMillis;
                buzzerState = !buzzerState;
                if (buzzerState) {
                    tone(BUZZER_PIN, 1000); // 1000 Hz pitch
                } else {
                    noTone(BUZZER_PIN);
                }
            }

        } else if (distance > DIST_STOP) {
            // Alarm zone: Dynamic fast beep
            lcd.print("Status: ALARM   ");

            // Map the distance to a beep interval (closer = faster beep)
            // 50cm -> 400ms interval, 10cm -> 50ms interval
            int beepInterval = map(distance, DIST_STOP, DIST_ALARM, 50, 400);

            if (currentMillis - previousBuzzerMillis >= beepInterval) {
                previousBuzzerMillis = currentMillis;
                buzzerState = !buzzerState;
                if (buzzerState) {
                    tone(BUZZER_PIN, 1500); // 1500 Hz pitch
                } else {
                    noTone(BUZZER_PIN);
                }
            }

        } else {
            // Stop zone: Continuous tone
            lcd.print("Status: STOP!   ");
            tone(BUZZER_PIN, 2000); // 2000 Hz high pitch alert
        }
    }

    // Small delay to stabilize the loop and prevent LCD flickering
    delay(50);
}

Build/Flash/Run commands

We will use the Arduino CLI to compile and upload the code. Open your terminal, navigate to the directory containing the ParkingAlarm folder, and execute the following commands.

Note: Replace /dev/ttyACM0 with your actual serial port (e.g., COM3 on Windows, or /dev/cu.usbmodem14101 on macOS).

# Update the core index
arduino-cli core update-index

# Install the AVR core
arduino-cli core install arduino:avr

# Compile the sketch
arduino-cli compile --fqbn arduino:avr:uno ParkingAlarm

# Upload to the board
arduino-cli upload --fqbn arduino:avr:uno --port /dev/ttyACM0 ParkingAlarm

# Monitor serial output
arduino-cli monitor --port /dev/ttyACM0 --config baudrate=115200

Step-by-step Validation

Follow these checkpoints to ensure your prototype is functioning correctly and meets the performance claims.

  1. LCD Initialization Check
    • Action: Apply power to the Arduino via USB.
    • Expected Evidence: The LCD backlight illuminates, and the text “Parking Assist” followed by “System Booting..” appears for 2 seconds before clearing. Adjust the potentiometer if the text is not visible.
  2. Distance Accuracy Validation
    • Action: Place a flat, rigid object (like a piece of cardboard or a book) exactly 50 cm away from the sensor, measured using a physical tape measure.
    • Expected Evidence: The LCD should output Dist: 50 cm (± 1-2 cm tolerance is acceptable due to the speed of sound varying slightly with ambient temperature).
  3. 10 Hz Update Rate Validation
    • Action: Observe the Serial Monitor output timestamps.
    • Expected Evidence: The delay(50) plus the sensor timeout bounds the loop to roughly 100ms per cycle. You should see approximately 10 distance readings printed to the serial console every second, confirming the 10 Hz target rate.
  4. Acoustic Zone Validation
    • Action: Move the rigid object progressively closer to the sensor, starting from 150 cm down to 5 cm.
    • Expected Evidence:
      • > 100 cm: LCD reads “Status: SAFE”, buzzer is completely silent.
      • 99 cm to 51 cm: LCD reads “Status: SLOW”, buzzer emits a steady 1 beep per second.
      • 50 cm to 11 cm: LCD reads “Status: ALARM”, buzzer beep rate increases dynamically as the object gets closer.
      • <= 10 cm: LCD reads “Status: STOP!”, buzzer emits a continuous, unbroken high-pitched tone.

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 is the primary objective of the prototype described in the article?




Question 2: What type of feedback does the parking distance alarm provide?




Question 3: Which function is used to implement non-blocking logic in the system?




Question 4: What is a practical use case mentioned for this prototype?




Question 5: At what distance does the buzzer produce a solid continuous tone?




Question 6: What is the expected latency between object detection and buzzer response?




Question 7: What metric is displayed on the LCD interface?




Question 8: Why is non-blocking logic preferred over delay() calls in this project?




Question 9: What commercial technology does this prototype demonstrate the core principles of?




Question 10: At what frequency can the system perform concurrent sensor reads and display updates without freezing?




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

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

Follow me:


Practical case: SD Event Data Logger with Arduino UNO

Practical case: SD Event Data Logger with Arduino UNO — hero

Objective and use case

What you’ll build: You will build a standalone, hardware-based event data logger that records manual discrete triggers directly to a local microSD card. Each recorded event is reliably saved with a sequential ID and a precise millisecond-resolution timestamp.

Why it matters / Use cases

  • Inventory and Warehouse Counting: Log physical item counts in isolated locations where Wi-Fi or RF infrastructure is unavailable.
  • Machine Cycle Tracking: Attach a mechanical limit switch to monitor and log the exact number of times an industrial machine completes a physical cycle.
  • Access Logging: Interface with a magnetic reed switch to record the exact timing and frequency of door openings independent of networked security.
  • Offline Field Data Collection: Enable environmental researchers to manually register observational events during remote field traverses.

Expected outcome

  • A fully debounced mechanical input (e.g., 50ms software debounce) that prevents single presses from registering as multiple false triggers.
  • Reliable, offline data persistence to a microSD card via SPI with low-latency (<10ms) write cycles.
  • Accurate, sequential event logs formatted as CSV for easy post-processing.

Audience: Embedded systems developers and hardware engineers; Level: Intermediate

Architecture/flow: Mechanical Switch (Input) → Microcontroller (Hardware Interrupt + Debounce Logic) → Millisecond Timer → SPI Interface → microSD Card (CSV Log)

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: 3 sections, 3 tables and 2 code blocks detected before publication.
  • Checked code: 1 Python/py_compile, 1 Arduino/arduino-cli compile.
  • Supported catalog: the article text was checked against Prometeo’s validation-capable device profiles, and 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 prototype, not a certified product. Before powering the setup, verify the pinout of your exact ULX3S board revision, keep FPGA I/O signals at 3.3 V, never connect 5 V directly to I/O pins, disconnect power before changing wiring, and use suitable external supplies for loads, motors or servos while sharing ground only when the wiring requires it.

Conceptual block diagram

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

Functional architecture

Mechanical Switch (Input)

Microcontroller

Millisecond Timer

SPI Interface

microSD Card (CSV Log)

Conceptual signal and responsibility flow between device blocks.

Validation path

Sketch

arduino-cli compile

Upload

Functional test

Conceptual summary of the tools used to check the published material.

Prerequisites

Before starting this project, ensure you have the following ready:
* A basic understanding of Arduino General Purpose Input/Output (GPIO) operations, specifically digital reads and writes.
* Familiarity with the concept of switch bouncing and why mechanical contacts require software or hardware debouncing.
* A computer running Windows, macOS, or Linux with a command-line interface terminal.
* The arduino-cli (Arduino Command Line Interface) installed and added to your system’s PATH.
* A microSD card (32GB or smaller) freshly formatted to FAT16 or FAT32. The standard Arduino SD library does not support exFAT formats typically found on 64GB+ cards.

Materials

  • Microcontroller: Arduino UNO R3 (ATmega328P).
  • Storage: microSD SPI module (Ensure it is a module with a built-in 3.3V voltage regulator and logic level shifter, as the Arduino UNO R3 operates at 5V logic).
  • Input: Standard 4-pin or 2-pin tactile momentary pushbutton.
  • Feedback: 5mm or 3mm Status LED (any color).
  • Passives: 1x 220-ohm resistor (for the LED). We will utilize the Arduino’s internal pull-up resistor for the pushbutton.
  • Prototyping: Breadboard and assorted male-to-male jumper wires.
  • Data/Power: USB Type A to Type B cable.

Setup/Connection

The Arduino UNO R3 communicates with the microSD module using the Serial Peripheral Interface (SPI) protocol. The ATmega328P has dedicated hardware SPI pins which must be used for optimal performance.

MicroSD SPI Module Wiring:

microSD Module Pin Arduino UNO R3 Pin Function Description
VCC 5V Power supply (module steps this down to 3.3V)
GND GND Common ground
MISO Pin 12 Master In Slave Out (Data from SD to Arduino)
MOSI Pin 11 Master Out Slave In (Data from Arduino to SD)
SCK Pin 13 Serial Clock (Timing signal generated by Arduino)
CS Pin 4 Chip Select (Signals the SD card to listen)

Pushbutton Wiring:
* Connect one terminal of the pushbutton to Pin 2 on the Arduino.
* Connect the opposite terminal of the pushbutton to GND.
* Note: No external resistor is required. We will configure Pin 2 using INPUT_PULLUP in the code, which connects an internal 20k-ohm resistor to 5V inside the ATmega328P. When the button is pressed, the pin reads LOW.

Status LED Wiring:
* Connect the longer leg (Anode) of the Status LED to Pin 8 on the Arduino.
* Connect the shorter leg (Cathode) to one end of the 220-ohm resistor.
* Connect the other end of the 220-ohm resistor to GND.

Validated Code

The following section contains the complete source code required for the event logger, as well as a supplementary Python script for analyzing the generated data.

Arduino Sketch: SD_Event_Logger.ino

Create a new directory named SD_Event_Logger and save the following code inside it as SD_Event_Logger.ino.

This code handles the debouncing of the mechanical button, initializes the SPI communication, and appends the data to the SD card. It uses the standard SD.h and SPI.h libraries included with the Arduino core.

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

/*
 * SD Event Data Logger
 * Target: Arduino UNO R3 (ATmega328P)
 * Objective: Log debounced button presses to a microSD card over SPI.
 */

#include <SPI.h>
#include <SD.h>

// Pin Definitions
const int chipSelect = 4;
const int buttonPin = 2;
const int ledPin = 8;

// State Variables
unsigned long eventCount = 0;
int buttonState;
int lastButtonState = HIGH; // HIGH because we use INPUT_PULLUP

// Timing Variables for Debounce
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50; // 50 milliseconds

void setup() {
  // Initialize Serial for debugging
  Serial.begin(9600);
  while (!Serial) {
    ; // Wait for serial port to connect
  }

  // Configure Pins
  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(ledPin, OUTPUT);

  Serial.println("Initializing SD card...");

  // Initialize SD Card
  if (!SD.begin(chipSelect)) {
    Serial.println("Critical Error: SD card initialization failed!");
    Serial.println("Check wiring, formatting (FAT16/32), and card insertion.");
    // Trap execution in an infinite loop and blink LED rapidly
    while (true) {
      digitalWrite(ledPin, HIGH);
      delay(100);
      digitalWrite(ledPin, LOW);
      delay(100);
    }
  }

  Serial.println("SD card initialized successfully.");

  // Optional: Write a CSV header if the file does not exist
  if (!SD.exists("events.csv")) {
    File dataFile = SD.open("events.csv", FILE_WRITE);
    if (dataFile) {
      dataFile.println("Event_ID,Uptime_ms");
      dataFile.close();
      Serial.println("Created new events.csv with headers.");
    } else {
      Serial.println("Error: Could not create events.csv");
    }
  }
}
// ... continues for members in the complete validated source ...

🔒 Part of the validated code is premium. With the 7-day pass or the monthly membership you can view the complete validated source.

/*
 * SD Event Data Logger
 * Target: Arduino UNO R3 (ATmega328P)
 * Objective: Log debounced button presses to a microSD card over SPI.
 */

#include <SPI.h>
#include <SD.h>

// Pin Definitions
const int chipSelect = 4;
const int buttonPin = 2;
const int ledPin = 8;

// State Variables
unsigned long eventCount = 0;
int buttonState;
int lastButtonState = HIGH; // HIGH because we use INPUT_PULLUP

// Timing Variables for Debounce
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50; // 50 milliseconds

void setup() {
  // Initialize Serial for debugging
  Serial.begin(9600);
  while (!Serial) {
    ; // Wait for serial port to connect
  }

  // Configure Pins
  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(ledPin, OUTPUT);

  Serial.println("Initializing SD card...");

  // Initialize SD Card
  if (!SD.begin(chipSelect)) {
    Serial.println("Critical Error: SD card initialization failed!");
    Serial.println("Check wiring, formatting (FAT16/32), and card insertion.");
    // Trap execution in an infinite loop and blink LED rapidly
    while (true) {
      digitalWrite(ledPin, HIGH);
      delay(100);
      digitalWrite(ledPin, LOW);
      delay(100);
    }
  }

  Serial.println("SD card initialized successfully.");

  // Optional: Write a CSV header if the file does not exist
  if (!SD.exists("events.csv")) {
    File dataFile = SD.open("events.csv", FILE_WRITE);
    if (dataFile) {
      dataFile.println("Event_ID,Uptime_ms");
      dataFile.close();
      Serial.println("Created new events.csv with headers.");
    } else {
      Serial.println("Error: Could not create events.csv");
    }
  }
}

void loop() {
  // Read the state of the switch into a local variable:
  int reading = digitalRead(buttonPin);

  // Check to see if you just pressed the button
  // (i.e. the input went from HIGH to LOW), and you've waited long enough
  // since the last press to ignore any noise:

  // If the switch changed, due to noise or pressing:
  if (reading != lastButtonState) {
    // reset the debouncing timer
    lastDebounceTime = millis();
  }

  if ((millis() - lastDebounceTime) > debounceDelay) {
    // whatever the reading is at, it's been there for longer than the debounce
    // delay, so take it as the actual current state:

    // if the button state has changed:
    if (reading != buttonState) {
      buttonState = reading;

      // only trigger an event if the new button state is LOW (pressed)
      if (buttonState == LOW) {
        logEventToSD();
      }
    }
  }

  // save the reading. Next time through the loop, it'll be the lastButtonState:
  lastButtonState = reading;
}

void logEventToSD() {
  eventCount++;
  unsigned long currentTimestamp = millis();

  // Open the file. Note that only one file can be open at a time.
  // The filename must follow the 8.3 format (max 8 chars name, 3 chars extension)
  File dataFile = SD.open("events.csv", FILE_WRITE);

  // If the file is available, write to it:
  if (dataFile) {
    dataFile.print(eventCount);
    dataFile.print(",");
    dataFile.println(currentTimestamp);
    dataFile.close();

    // Print to the serial port too:
    Serial.print("Logged -> Event: ");
    Serial.print(eventCount);
    Serial.print(" | Time: ");
    Serial.println(currentTimestamp);

    // Visual feedback: Quick pulse on the status LED
    digitalWrite(ledPin, HIGH);
    delay(150); // Short blocking delay is acceptable here to stretch the visual flash
    digitalWrite(ledPin, LOW);
  } 
  else {
    // If the file isn't open, pop up an error:
    Serial.println("Error: Failed to open events.csv for writing.");

    // Visual feedback: Three slow flashes indicating a write error
    for (int i = 0; i < 3; i++) {
      digitalWrite(ledPin, HIGH);
      delay(300);
      digitalWrite(ledPin, LOW);
      delay(300);
    }
  }
}

Python Analysis Script: analyze_events.py

Save this file on your computer as analyze_events.py. Once you have collected data on your SD card, insert the SD card into your computer and run this script against the events.csv file to parse the basic metrics.

#!/usr/bin/env python3
"""
SD Event Logger Analysis Tool
Objective: Parse the events.csv file generated by the Arduino UNO R3 prototype
and calculate basic duration metrics.
"""

import csv
import sys

def analyze_log(filename):
    try:
        with open(filename, 'r') as file:
            reader = csv.reader(file)
            header = next(reader) # Skip the header row

            events = list(reader)

        total_events = len(events)
        print(f"--- Log Analysis for {filename} ---")
        print(f"Total discrete events logged: {total_events}")

        if total_events >= 2:
            first_time_ms = int(events[0][1])
            last_time_ms = int(events[-1][1])

            duration_ms = last_time_ms - first_time_ms
            duration_sec = duration_ms / 1000.0

            print(f"First event registered at: {first_time_ms} ms")
            print(f"Last event registered at:  {last_time_ms} ms")
            print(f"Total duration between first and last event: {duration_sec:.2f} seconds")

            if duration_sec > 0:
                frequency = total_events / duration_sec
                print(f"Average event frequency: {frequency:.2f} events/second")

    except FileNotFoundError:
        print(f"Error: Could not find '{filename}'. Ensure the path is correct.")
    except ValueError as ve:
        print(f"Error parsing data (likely malformed CSV row): {ve}")
    except Exception as e:
        print(f"An unexpected error occurred: {e}")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python analyze_events.py <path_to_events.csv>")
    else:
        analyze_log(sys.argv[1])

Build/Flash/Run commands

Use the Arduino CLI to compile and upload the code. Connect your Arduino UNO R3 to your computer via USB. Identify the port (e.g., COM3 on Windows or /dev/ttyACM0 on Linux/macOS).

Command Purpose CLI Command
Update core index arduino-cli core update-index
Install AVR core arduino-cli core install arduino:avr
Compile sketch arduino-cli compile --fqbn arduino:avr:uno SD_Event_Logger
Upload to board arduino-cli upload --fqbn arduino:avr:uno --port <PORT> SD_Event_Logger
Monitor serial arduino-cli monitor --port <PORT> --config baudrate=9600

Workflow:
1. Open your terminal and navigate to the parent directory containing the SD_Event_Logger folder.
2. Execute the index update and core installation commands to ensure your environment is ready.
3. Compile the sketch using the --fqbn flag for the UNO.
4. Replace <PORT> with your actual serial port and run the upload command.
5. Immediately launch the serial monitor to observe the initialization process.

Step-by-step Validation

Follow these checkpoints to ensure your prototype is functioning correctly.

  1. SD Initialization Check
    • Observation: Open the serial monitor immediately after powering the board.
    • Pass condition: The serial monitor displays “Initializing SD card…” followed by “SD card initialized successfully.” The status LED remains off.
  2. Missing Card Error Handling Check
    • Observation: Remove power, eject the microSD card, restore power, and watch the status LED.
    • Pass condition: The serial monitor displays “Critical Error: SD card initialization failed!” and the status LED blinks rapidly and continuously.
  3. Event Trigger Check
    • Observation: Reinsert the SD card, power the board, and press the pushbutton once.
    • Pass condition: The status LED pulses briefly (150ms). The serial monitor outputs Logged -> Event: 1 | Time: [timestamp].
  4. Debounce Logic Check
    • Observation: Press and hold the pushbutton, wiggle it slightly without fully releasing, then release.
    • Pass condition: Only one event is logged per distinct press-and-release cycle. The event count increments smoothly without skipping numbers (e.g., jumping from 1 to 4).
  5. Data Integrity Check
    • Observation: Power off the Arduino. Remove the SD card, insert it into your computer, and run the Python analysis script: python analyze_events.py /path/to/SD/events.csv.
    • Pass condition: The script successfully parses the file, reporting the correct total number of events and calculating the time duration between the first and last press.

Troubleshooting

Symptom Likely cause Fix
SD initialization fails (Rapid

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 is the primary function of the device described in the article?




Question 2: What specific details are reliably saved with each recorded event?




Question 3: How does the device support inventory and warehouse counting?




Question 4: What component is suggested to monitor and log the exact number of times an industrial machine completes a cycle?




Question 5: What is the purpose of the 50ms software debounce?




Question 6: Which interface is used to persist data to the microSD card?




Question 7: What is the expected write cycle latency for saving data to the microSD card?




Question 8: What component is used for access logging to record door openings?




Question 9: How can environmental researchers use this device for offline field data collection?




Question 10: What is a key characteristic of the device's operation regarding network connectivity?




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

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

Follow me:


Practical case: Arduino UNO Servo Calibration Tester

Practical case: Arduino UNO Servo Calibration Tester — hero

Objective and use case

What you’ll build: A standalone, multi-mode servo calibration tester that allows users to manually sweep, automatically center (1500µs), and test the physical limits of standard hobby servos.

Why it matters / Use cases

  • RC Vehicle Setup: Perfectly center steering and throttle servos before attaching mechanical servo horns to ensure symmetrical steering geometry.
  • Robotic Arm Assembly: Identify exact PWM microsecond values (e.g., 500µs to 2500µs) for physical joint limits to prevent hard mechanical binding and motor burnout in your code.
  • Hardware Diagnostics: Quickly test salvaged or suspect servos for dead spots, gear stripping, or excessive jitter without needing to write or flash a custom test script.
  • Rapid Prototyping: Establish a permanent benchtop tool to manually actuate linkages and test mechanisms during the mechanical design phase.

Expected outcome

  • A reliable hardware control interface utilizing an ADC-read potentiometer for smooth, low-latency manual angle adjustments.
  • A software-debounced pushbutton input that seamlessly cycles the microcontroller state machine through three operational modes: Manual Control, Center (90°), and Auto Sweep.

Audience: Hardware hobbyists, robotics developers, and RC builders; Level: Intermediate

Architecture/flow: User Inputs (Potentiometer & Pushbutton) → Microcontroller (ADC Reading & Debounce Logic) → 50Hz PWM Signal Output → Servo Motor Actuation

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: 3 sections, 3 tables and 2 code blocks detected before publication.
  • Checked code: 1 Arduino/arduino-cli compile, 1 Bash/copy-paste checks.
  • Supported catalog: the article text was checked against Prometeo’s validation-capable device profiles, and 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 prototype is designed strictly for educational laboratory use and benchtop calibration of small, unloaded hobby servos.
* Power Limits: Do not attempt to power high-torque servos (e.g., MG995, MG996R) or multi-servo robotic arms directly from the Arduino UNO’s 5V pin. Doing so will exceed the onboard voltage regulator’s thermal and current limits, potentially destroying the Arduino board or your computer’s USB port.
* Mechanical Hazards: Even small servos can present a pinch hazard if attached to rigid mechanical linkages. Always perform initial calibration tests with the servo horn removed or completely free of mechanical obstructions.
* Suitability: This device is not suitable for, nor intended to be used in, critical control systems, aerospace RC applications, or any environment where failure could result in property damage or injury.

Conceptual block diagram

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

Functional architecture

User Inputs (Potentiometer & Pushbutton)

Microcontroller (ADC Reading & Debounce L…

50Hz PWM Signal Output

Servo Motor Actuation

Conceptual signal and responsibility flow between device blocks.

Validation path

Sketch

arduino-cli compile

Upload

Functional test

Conceptual summary of the tools used to check the published material.

Prerequisites

To successfully complete this tutorial, you should have:
* A basic understanding of how to use a terminal or command prompt.
* The Arduino CLI installed on your workstation.
* Familiarity with basic breadboard prototyping (understanding continuity along power rails and terminal strips).
* Understanding of basic Pulse Width Modulation (PWM) concepts, specifically the standard 20 millisecond period and 1–2 millisecond duty cycle used by standard hobby servos.

Materials

For this prototype, you will need the following exact components:
* Arduino UNO R3 (ATmega328P): The primary microcontroller board.
* SG90 Micro Servo: A standard 9-gram hobby servo motor.
* 10 kΩ Potentiometer: A standard rotary potentiometer (linear taper preferred).
* Pushbutton: A standard momentary tactile switch.
* Breadboard and Jumper Wires: For making solderless connections.
* USB Type A to Type B Cable: To connect the Arduino UNO R3 to your computer.

Setup/Connection

The hardware setup is designed to be minimal, utilizing the Arduino’s internal pull-up resistor for the pushbutton to reduce external component count.

Component Wiring Guide:

Component Pin / Terminal Arduino UNO R3 Connection Notes
SG90 Servo Brown / Black Wire GND Ground connection.
SG90 Servo Red Wire 5V Power connection (Unloaded testing only).
SG90 Servo Orange / Yellow Wire Digital Pin 9 PWM control signal.
Potentiometer Left Terminal (Pin 1) 5V Reference voltage for the analog read.
Potentiometer Middle Terminal (Wiper) Analog Pin A0 Variable voltage output (0-5V).
Potentiometer Right Terminal (Pin 3) GND Ground reference.
Pushbutton Terminal 1 Digital Pin 2 Input signal.
Pushbutton Terminal 2 GND Closes circuit to ground when pressed.

Note on the Pushbutton: We connect the button directly between Digital Pin 2 and Ground. In the software, we will configure Pin 2 with INPUT_PULLUP. This holds the pin at a HIGH logic level (5V) internally. When the button is pressed, it connects the pin to ground, driving the logic level LOW.

Note on the Servo Power: The SG90 is a small servo. When completely unloaded (no mechanical linkages attached), it can safely draw power from the Arduino UNO’s 5V pin, which is supplied by the USB connection. If you attach a mechanical load to the servo, it will draw more current and may cause the Arduino to reset.

Validated Code

The project requires two files. The first is the main Arduino sketch containing the logic. The second is a simple shell script to automate the compilation and upload process using the Arduino CLI.

Main Sketch: servo_tester.ino

Create a directory named servo_tester and save the following code inside it as servo_tester.ino.

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

/*
 * Project: Servo Calibration Tester
 * Target: Arduino UNO R3 (ATmega328P)
 * Description: A multi-mode servo testing tool. Uses a potentiometer for manual
 * control and a pushbutton to cycle between Manual, Center, and Sweep modes.
 */

#include <Servo.h>

// Pin Definitions
const int SERVO_PIN = 9;
const int POT_PIN = A0;
const int BUTTON_PIN = 2;

// Servo Object
Servo testServo;

// State Machine Variables
enum TesterMode {
  MODE_MANUAL = 0,
  MODE_CENTER = 1,
  MODE_SWEEP  = 2
};

TesterMode currentMode = MODE_MANUAL;

// Button Debouncing Variables
int buttonState = HIGH;             // Current reading from the input pin
int lastButtonState = HIGH;         // Previous reading from the input pin
unsigned long lastDebounceTime = 0; // The last time the output pin was toggled
const unsigned long debounceDelay = 50;   // Debounce time in milliseconds

// Servo Sweep Variables
int sweepAngle = 0;
int sweepDirection = 1;
unsigned long lastSweepUpdate = 0;
const unsigned long sweepInterval = 15; // Milliseconds between sweep steps

// Telemetry Variables
unsigned long lastTelemetryTime = 0;
const unsigned long telemetryInterval = 500; // Update Serial every 500ms

void setup() {
  // Initialize Serial Monitor
  Serial.begin(115200);
  while (!Serial) {
    ; // Wait for serial port to connect
  }

  Serial.println("Initializing Servo Calibration Tester...");

  // Configure Pins
  pinMode(POT_PIN, INPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP); // Use internal pull-up resistor

  // Attach Servo
  // Using standard min/max pulse widths (1000us to 2000us)
  // Some SG90 servos respond better to 500us to 2400us, adjust if necessary.
  testServo.attach(SERVO_PIN, 1000, 2000);

  // Set initial position
  testServo.write(90);
  Serial.println("System Ready. Mode: MANUAL");
}

void loop() {
  handleButton();
  updateServo();
  sendTelemetry();
}

void handleButton() {
  int reading = digitalRead(BUTTON_PIN);

  // Check if the button state has changed
  if (reading != lastButtonState) {
    lastDebounceTime = millis();
  }
// ... continues for members in the complete validated source ...

🔒 Part of the validated code is premium. With the 7-day pass or the monthly membership you can view the complete validated source.

/*
 * Project: Servo Calibration Tester
 * Target: Arduino UNO R3 (ATmega328P)
 * Description: A multi-mode servo testing tool. Uses a potentiometer for manual
 * control and a pushbutton to cycle between Manual, Center, and Sweep modes.
 */

#include <Servo.h>

// Pin Definitions
const int SERVO_PIN = 9;
const int POT_PIN = A0;
const int BUTTON_PIN = 2;

// Servo Object
Servo testServo;

// State Machine Variables
enum TesterMode {
  MODE_MANUAL = 0,
  MODE_CENTER = 1,
  MODE_SWEEP  = 2
};

TesterMode currentMode = MODE_MANUAL;

// Button Debouncing Variables
int buttonState = HIGH;             // Current reading from the input pin
int lastButtonState = HIGH;         // Previous reading from the input pin
unsigned long lastDebounceTime = 0; // The last time the output pin was toggled
const unsigned long debounceDelay = 50;   // Debounce time in milliseconds

// Servo Sweep Variables
int sweepAngle = 0;
int sweepDirection = 1;
unsigned long lastSweepUpdate = 0;
const unsigned long sweepInterval = 15; // Milliseconds between sweep steps

// Telemetry Variables
unsigned long lastTelemetryTime = 0;
const unsigned long telemetryInterval = 500; // Update Serial every 500ms

void setup() {
  // Initialize Serial Monitor
  Serial.begin(115200);
  while (!Serial) {
    ; // Wait for serial port to connect
  }

  Serial.println("Initializing Servo Calibration Tester...");

  // Configure Pins
  pinMode(POT_PIN, INPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP); // Use internal pull-up resistor

  // Attach Servo
  // Using standard min/max pulse widths (1000us to 2000us)
  // Some SG90 servos respond better to 500us to 2400us, adjust if necessary.
  testServo.attach(SERVO_PIN, 1000, 2000);

  // Set initial position
  testServo.write(90);
  Serial.println("System Ready. Mode: MANUAL");
}

void loop() {
  handleButton();
  updateServo();
  sendTelemetry();
}

void handleButton() {
  int reading = digitalRead(BUTTON_PIN);

  // Check if the button state has changed
  if (reading != lastButtonState) {
    lastDebounceTime = millis();
  }

  // If the state has been stable longer than the debounce delay
  if ((millis() - lastDebounceTime) > debounceDelay) {
    // If the button state has physically changed
    if (reading != buttonState) {
      buttonState = reading;

      // Only toggle mode if the new button state is LOW (pressed)
      if (buttonState == LOW) {
        cycleMode();
      }
    }
  }
  lastButtonState = reading;
}

void cycleMode() {
  if (currentMode == MODE_MANUAL) {
    currentMode = MODE_CENTER;
    Serial.println(">>> Mode Switched: CENTER (90 deg) <<<");
  } else if (currentMode == MODE_CENTER) {
    currentMode = MODE_SWEEP;
    Serial.println(">>> Mode Switched: AUTO-SWEEP <<<");
  } else {
    currentMode = MODE_MANUAL;
    Serial.println(">>> Mode Switched: MANUAL <<<");
  }
}

void updateServo() {
  switch (currentMode) {
    case MODE_MANUAL:
      {
        int potValue = analogRead(POT_PIN);
        // Map 10-bit ADC (0-1023) to Servo Angle (0-180)
        int targetAngle = map(potValue, 0, 1023, 0, 180);
        testServo.write(targetAngle);
      }
      break;

    case MODE_CENTER:
      {
        testServo.write(90);
      }
      break;

    case MODE_SWEEP:
      {
        if (millis() - lastSweepUpdate > sweepInterval) {
          lastSweepUpdate = millis();
          sweepAngle += sweepDirection;

          if (sweepAngle >= 180) {
            sweepAngle = 180;
            sweepDirection = -1;
          } else if (sweepAngle <= 0) {
            sweepAngle = 0;
            sweepDirection = 1;
          }
          testServo.write(sweepAngle);
        }
      }
      break;
  }
}

void sendTelemetry() {
  if (millis() - lastTelemetryTime > telemetryInterval) {
    lastTelemetryTime = millis();

    int currentAngle = testServo.read();
    int currentMicroseconds = testServo.readMicroseconds();

    Serial.print("Mode: ");
    switch (currentMode) {
      case MODE_MANUAL: Serial.print("MANUAL\t"); break;
      case MODE_CENTER: Serial.print("CENTER\t"); break;
      case MODE_SWEEP:  Serial.print("SWEEP \t"); break;
    }

    Serial.print(" | Angle: ");
    Serial.print(currentAngle);
    Serial.print(" deg | Pulse: ");
    Serial.print(currentMicroseconds);
    Serial.println(" us");
  }
}

Build Script: build.sh

Save this file in the parent directory of servo_tester (or adjust the path accordingly). Make it executable with chmod +x build.sh. Replace <PORT> with your actual serial port (e.g., /dev/ttyACM0 on Linux, COM3 on Windows).

#!/bin/bash

PORT="/dev/ttyACM0" # CHANGE THIS TO YOUR ACTUAL PORT
FQBN="arduino:avr:uno"
SKETCH_DIR="servo_tester"

echo "Updating Arduino core index..."
arduino-cli core update-index

echo "Installing AVR core if not present..."
arduino-cli core install arduino:avr

echo "Compiling sketch..."
arduino-cli compile --fqbn $FQBN $SKETCH_DIR

if [ $? -eq 0 ]; then
    echo "Compilation successful. Uploading to $PORT..."
    arduino-cli upload --fqbn $FQBN --port $PORT $SKETCH_DIR
    echo "Upload complete. Open your serial monitor at 115200 baud."
else
    echo "Compilation failed. Aborting upload."
    exit 1
fi

Build/Flash/Run commands

To compile and upload the firmware manually (if you choose not to use the provided shell script), follow this standardized workflow using the Arduino CLI.

Command Reference

Action Command
Update Index arduino-cli core update-index
Install Core arduino-cli core install arduino:avr
Compile arduino-cli compile --fqbn arduino:avr:uno servo_tester
Upload arduino-cli upload --fqbn arduino:avr:uno --port <PORT> servo_tester
Monitor arduino-cli monitor --port <PORT> --config baudrate=115200

Workflow

  1. Identify your port: Connect your Arduino UNO R3 to your computer. Run arduino-cli board list to find the port identifier (e.g., /dev/ttyUSB0, /dev/ttyACM0, or COM3).
  2. Prepare the environment: Run the core update and install commands to ensure your system has the latest ATmega328P toolchain.
  3. Compile the code: Execute the compile command targeting the servo_tester directory. Verify that no syntax errors are returned.
  4. Flash the device: Execute the upload command, replacing <PORT> with the port identified in step 1.
  5. Monitor the output: Launch the serial monitor command to observe the boot sequence and telemetry.

Step-by-step Validation

Once the code is uploaded and the serial monitor is open, perform the following grouped checks to validate the system.

  • Checkpoint 1: Power-on and Initialization

    • Action: Reset the Arduino.
    • Expected Observation: The serial monitor displays “Initializing Servo Calibration Tester…” followed by “System Ready. Mode: MANUAL”. The servo should snap to an initial 90-degree position.
    • Pass Condition: Telemetry begins printing every 500ms showing “Mode: MANUAL”.
  • Checkpoint 2: Manual Sweep Mode

    • Action: While in MANUAL mode, turn the potentiometer from the far left to the far right.
    • Expected Observation: The servo horn rotates smoothly in tandem with the potentiometer movement.
    • Pass Condition: The serial monitor reports angles changing smoothly from ~0° to ~180°, and pulse widths changing from ~1000 µs to ~2000 µs.
  • Checkpoint 3: Mode Switching & Centering

    • Action: Press the pushbutton once.
    • Expected Observation: The serial monitor outputs “>>> Mode Switched: CENTER (90 deg) <<<“. The servo immediately moves to its mechanical center point and locks there.
    • Pass Condition: Turning the potentiometer now has no effect on the servo. Telemetry outputs exactly 90 deg and 1500 µs.
  • Checkpoint 4: Auto-Sweep Mode

    • Action: Press the pushbutton a second time.
    • Expected Observation: The serial monitor outputs “>>> Mode Switched: AUTO-SWEEP <<<“. The servo begins oscillating back and forth automatically.
    • Pass Condition: The telemetry shows the angle incrementing to 180, reversing, decrementing to 0, and repeating.
  • Checkpoint 5: Return to Manual

    • Action: Press the pushbutton a third time.
    • Expected Observation: The system returns to MANUAL mode.
    • Pass Condition: The servo immediately snaps to the angle currently dictated by the physical position of the potentiometer wiper.

Troubleshooting

Symptom Likely Cause Fix
Servo jitters erratically Unstable power supply or poor ground connection. Ensure all ground pins (Arduino, Potentiometer, Servo) are tied to a common rail. Check jumper wires.
Arduino resets when servo moves Servo is drawing too much current (brownout). The servo is under mechanical load. Disconnect the load, or power the servo from an external 5V source (sharing grounds).
Button press is ignored / double-triggers Debounce delay is too short or wiring is loose. Increase debounceDelay in the code to 100ms. Verify the button is firmly seated in the breadboard.
Servo only turns 90 degrees total PWM pulse width mapping mismatch. Change testServo.attach(SERVO_PIN, 1000, 2000) to testServo.attach(SERVO_PIN, 500, 2400) to match your specific SG90’s limits.
Compile error: “Servo.h: No such file” Missing standard library. Run arduino-cli lib install Servo to ensure the standard library is available to the compiler.

Improvements

Once the base prototype is functioning, consider these grouped enhancements:

Hardware Upgrades
* External Power Supply: Add a dedicated 5V, 2A power supply (like a UBEC or bench supply) to the breadboard power rails to test heavy-duty, high-torque metal gear servos without browning out the Arduino.
* OLED Display: Add an I2C 0.96″ OLED screen to display the current angle and pulse width directly on the device, removing the need for a connected computer and serial monitor.

Software Enhancements
* Custom Pulse Width Tuning: Add a fourth mode that uses the potentiometer to finely adjust the minimum and maximum microsecond limits, allowing you to map the exact physical endpoints of off-brand servos.
* Speed Testing Mode: Implement a mode that commands instantaneous 0° to 180° jumps and times the physical response, helping to verify servo speed specifications.

Checklist

  • [ ] Arduino CLI is installed and the AVR core is updated.
  • [ ] Components are wired according to the Setup table (checking common ground).
  • [ ] Pushbutton is wired correctly to utilize the internal pull-up resistor.
  • [ ] servo_tester.ino is saved in the correct directory.
  • [ ] The sketch compiles successfully using arduino-cli compile.
  • [ ] The firmware uploads successfully to the correct serial port.
  • [ ] The Serial Monitor displays the initialization sequence at 115200 baud.
  • [ ] The servo responds smoothly to the potentiometer in MANUAL mode.
  • [ ] The pushbutton successfully cycles through all three operational modes.

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 is the primary objective of the project described in the article?




Question 2: What microsecond value is associated with automatically centering the servo?




Question 3: Why is it important to center steering and throttle servos in RC Vehicle Setup?




Question 4: In Robotic Arm Assembly, why should you identify exact PWM microsecond values for physical joint limits?




Question 5: What hardware component is used for smooth, low-latency manual angle adjustments?




Question 6: How many operational modes does the microcontroller state machine cycle through?




Question 7: Which of the following is NOT one of the operational modes mentioned in the text?




Question 8: What is a benefit of using this tester for hardware diagnostics?




Question 9: How does the tester assist in rapid prototyping?




Question 10: What is the function of the software-debounced pushbutton in this project?




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

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

Follow me:


Practical case: RFID lock with Arduino UNO R3

Practical case: RFID lock with Arduino UNO R3 — hero

Objective and use case

What you’ll build: A basic RFID-controlled door-lock prototype using an Arduino UNO R3, MFRC522 RFID reader, and 1-channel relay. When an authorized card or key fob is detected, the Arduino validates the UID and energizes the relay for 2–5 seconds to drive an electric strike, maglock interface, or low-voltage lock circuit.

Why it matters / Use cases

  • Small workshop or cabinet access prototype: Control access to a tool cabinet, lab drawer, or demo enclosure so only known RFID tags can unlock it.
  • Entry system concept demonstrator: Practice the same flow used in larger systems: credential read, UID match, timed unlock, and relay-based lock control.
  • Electromechanical integration practice: Combine SPI-based RFID sensing with a relay output, moving beyond LED-only projects into real actuator switching.
  • Serial-monitor troubleshooting and auditing: Print detected UIDs and allow/deny events over serial at 9600 baud for fast debugging and basic access logs during testing.

Expected outcome

  • An RFID card is detected and matched in typically under 200 ms from presentation to decision.
  • The relay switches on for a configurable unlock window, commonly 3 seconds, then returns to the locked state automatically.
  • Unauthorized tags are rejected immediately, with the UID shown in the Serial Monitor for enrollment or diagnostics.
  • The prototype runs comfortably on the UNO’s 16 MHz ATmega328P with low CPU load and no GPU requirement.

Audience: Arduino beginners, students, and embedded/electronics learners building access-control demos; Level: beginner to intermediate

Architecture/flow: MFRC522 reads tag UID over SPI → Arduino UNO compares UID against an authorized list in code → if matched, digital output activates 1-channel relay → relay drives lock interface for a timed interval → Serial Monitor reports allow/deny status and UID values.

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: 3 sections, 1 tables and 24 code blocks detected before publication.
  • Checked code: 7 C/C++ static checks, 12 Bash/copy-paste checks.
  • Supported catalog: the article text was checked against Prometeo’s validation-capable device profiles, and 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 prototype is for educational low-voltage access-control learning. It is not a certified security product and should not be trusted as the sole protection for homes, businesses, hazardous areas, or life-safety exits.

Important limits and precautions:

  • Do not switch mains voltage directly unless you are trained and using properly rated hardware and enclosure practices.
  • For student use, validate the relay with a safe low-voltage load first.
  • RFID UID-only authorization is weak security.
  • Many low-cost RFID tags can be cloned, and UID checking alone is not secure enough for serious protection.
  • MFRC522 module voltage matters.
  • The reader is a 3.3 V device. Review your specific module’s logic tolerance before connecting it to 5 V Arduino SPI lines.
  • Real electric locks may require separate power and protection.
  • Follow the lock manufacturer’s current, voltage, and suppression requirements.
  • Do not use this project for emergency egress doors or safety-critical locks.
  • A software bug, relay failure, power loss, or wiring fault could prevent expected operation.
  • Mount and insulate wiring properly.
  • Loose wires can cause resets, false triggering, or damaged modules.

Conceptual block diagram

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

Functional architecture

MFRC522 reads tag UID over SPI

Arduino UNO compares UID against an autho…

if matched, digital output activates 1-ch…

relay drives lock interface for a timed i…

Serial Monitor reports allow/deny status…

Conceptual signal and responsibility flow between device blocks.

Validation path

Sketch

arduino-cli compile

Upload

Functional test

Conceptual summary of the tools used to check the published material.

Prerequisites

Before starting, you should be comfortable with:

  • Uploading a sketch to an Arduino UNO
  • Opening the Serial Monitor
  • Making simple jumper-wire connections on a breadboard or module headers
  • Understanding that a relay is an electrically controlled switch

You do not need prior RFID experience. This tutorial explains the minimum required to make a usable access-control prototype.

Materials

Use the exact device family and model requested:

  • Arduino UNO R3 (ATmega328P) + MFRC522 RFID module + 1-channel relay module

Recommended full parts list:

  • 1 x Arduino UNO R3 (ATmega328P)
  • 1 x MFRC522 RFID reader module with matching RFID card or key fob
  • 1 x 1-channel relay module
  • Prefer a module with transistor driver and opto-isolation or built-in flyback protection
  • Coil side usually powered from 5 V
  • Jumper wires
  • USB cable for Arduino UNO
  • Breadboard or stable wiring surface
  • Optional but strongly recommended for visible testing:
  • 1 x LED
  • 1 x 220 ohm resistor
  • Optional lock-side educational load:
  • Low-voltage lamp, LED indicator, or small DC load controlled through relay contacts
  • If you later connect a real electric strike or maglock interface, use the lock manufacturer’s recommended external power source

Setup/Connection

How the system works

The MFRC522 reads the UID of a nearby RFID tag over SPI.
The Arduino compares that UID against a small list of authorized UIDs stored in the sketch.
If the UID matches, the Arduino drives the relay module input for a short time.
The relay contacts can then switch a separate lock circuit or educational test load.

Important voltage note before wiring

The MFRC522 reader is a 3.3 V device. Most common breakout boards expose pins labeled SDA, SCK, MOSI, MISO, RST, 3.3V, and GND. On typical hobby tutorials, the module is powered from 3.3 V, while SPI lines connect directly to the UNO. Many students do this successfully in practice, but electrically the UNO outputs 5 V logic. If your MFRC522 board does not tolerate 5 V on logic pins, use proper level shifting. For a basic educational build, follow the common module usage pattern only if your specific module is known to work that way.

Pin mapping

Use the common MFRC522-to-UNO SPI wiring and one digital pin for the relay.

Module/Signal Arduino UNO R3 Pin Notes
MFRC522 SDA / SS D10 SPI slave select
MFRC522 SCK D13 SPI clock
MFRC522 MOSI D11 SPI MOSI
MFRC522 MISO D12 SPI MISO
MFRC522 RST D9 Reset pin for reader
MFRC522 3.3V 3.3V Do not use 5V for reader power
MFRC522 GND GND Common ground
Relay IN D7 Relay control signal
Relay VCC 5V Typical relay module power
Relay GND GND Common ground
Optional LED anode via 220 ohm resistor D6 Status indicator
Optional LED cathode GND LED return

Text-only connection steps

  1. Power off the Arduino before making or changing wiring.
  2. Connect the MFRC522:
  3. MFRC522 3.3V -> UNO 3.3V
  4. MFRC522 GND -> UNO GND
  5. MFRC522 SDA/SS -> UNO D10
  6. MFRC522 SCK -> UNO D13
  7. MFRC522 MOSI -> UNO D11
  8. MFRC522 MISO -> UNO D12
  9. MFRC522 RST -> UNO D9
  10. Connect the relay module:
  11. Relay VCC -> UNO 5V
  12. Relay GND -> UNO GND
  13. Relay IN -> UNO D7
  14. Optional visible status LED:
  15. UNO D6 -> 220 ohm resistor
  16. Resistor -> LED anode
  17. LED cathode -> GND
  18. If testing relay contacts with a safe low-voltage load:
  19. Use relay COM and NO for “normally off, on only when unlocked”
  20. Wire the low-voltage supply path through COM and NO
  21. Keep the switched circuit electrically appropriate for the relay ratings and module design

Relay contact concept

The relay module has two sides:

  • Control side
  • VCC, GND, IN
  • Connected to Arduino
  • Switching side
  • COM, NO, NC
  • Connected to the lock circuit or test load

For a door unlock action, COM + NO is usually the simplest:
– Idle: open circuit
– Authorized card: relay closes COM to NO for a few seconds

Validated Code

Arduino sketch: rfid_door_lock_relay.ino

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

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

static const uint8_t SS_PIN = 10;
static const uint8_t RST_PIN = 9;
static const uint8_t RELAY_PIN = 7;
static const uint8_t STATUS_LED_PIN = 6;

// Many relay modules are ACTIVE LOW.
// Set to true if your relay turns on when the Arduino pin goes LOW.
// Set to false if your relay turns on when the Arduino pin goes HIGH.
static const bool RELAY_ACTIVE_LOW = true;

// Unlock timing in milliseconds
static const unsigned long UNLOCK_TIME_MS = 3000;

// Example authorized UIDs.
// Replace these with the UIDs printed by your own cards during enrollment/testing.
const byte AUTHORIZED_UIDS[][4] = {
  {0xDE, 0xAD, 0xBE, 0xEF},
  {0x12, 0x34, 0x56, 0x78}
};
const size_t AUTHORIZED_UID_COUNT = sizeof(AUTHORIZED_UIDS) / sizeof(AUTHORIZED_UIDS[0]);

MFRC522 mfrc522(SS_PIN, RST_PIN);

bool relayActive = false;
unsigned long relayActivatedAt = 0;
String lastUidString = "";
unsigned long lastScanAt = 0;

// Ignore repeated reads of the same card within this time window
static const unsigned long SAME_CARD_DEBOUNCE_MS = 1500;

void setRelay(bool on) {
  relayActive = on;

  if (RELAY_ACTIVE_LOW) {
    digitalWrite(RELAY_PIN, on ? LOW : HIGH);
  } else {
    digitalWrite(RELAY_PIN, on ? HIGH : LOW);
  }

  digitalWrite(STATUS_LED_PIN, on ? HIGH : LOW);
}
// ... continues for members in the complete validated source ...

🔒 Part of the validated code is premium. With the 7-day pass or the monthly membership you can view the complete validated source.

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

static const uint8_t SS_PIN = 10;
static const uint8_t RST_PIN = 9;
static const uint8_t RELAY_PIN = 7;
static const uint8_t STATUS_LED_PIN = 6;

// Many relay modules are ACTIVE LOW.
// Set to true if your relay turns on when the Arduino pin goes LOW.
// Set to false if your relay turns on when the Arduino pin goes HIGH.
static const bool RELAY_ACTIVE_LOW = true;

// Unlock timing in milliseconds
static const unsigned long UNLOCK_TIME_MS = 3000;

// Example authorized UIDs.
// Replace these with the UIDs printed by your own cards during enrollment/testing.
const byte AUTHORIZED_UIDS[][4] = {
  {0xDE, 0xAD, 0xBE, 0xEF},
  {0x12, 0x34, 0x56, 0x78}
};
const size_t AUTHORIZED_UID_COUNT = sizeof(AUTHORIZED_UIDS) / sizeof(AUTHORIZED_UIDS[0]);

MFRC522 mfrc522(SS_PIN, RST_PIN);

bool relayActive = false;
unsigned long relayActivatedAt = 0;
String lastUidString = "";
unsigned long lastScanAt = 0;

// Ignore repeated reads of the same card within this time window
static const unsigned long SAME_CARD_DEBOUNCE_MS = 1500;

void setRelay(bool on) {
  relayActive = on;

  if (RELAY_ACTIVE_LOW) {
    digitalWrite(RELAY_PIN, on ? LOW : HIGH);
  } else {
    digitalWrite(RELAY_PIN, on ? HIGH : LOW);
  }

  digitalWrite(STATUS_LED_PIN, on ? HIGH : LOW);
}

void printUid(const MFRC522::Uid *uid) {
  for (byte i = 0; i < uid->size; i++) {
    if (uid->uidByte[i] < 0x10) {
      Serial.print("0");
    }
    Serial.print(uid->uidByte[i], HEX);
    if (i < uid->size - 1) {
      Serial.print(":");
    }
  }
}

String uidToString(const MFRC522::Uid *uid) {
  String s = "";
  for (byte i = 0; i < uid->size; i++) {
    if (uid->uidByte[i] < 0x10) {
      s += "0";
    }
    s += String(uid->uidByte[i], HEX);
    if (i < uid->size - 1) {
      s += ":";
    }
  }
  s.toUpperCase();
  return s;
}

bool isAuthorized(const MFRC522::Uid *uid) {
  // This basic example checks only 4-byte UIDs against a fixed list.
  if (uid->size != 4) {
    return false;
  }

  for (size_t i = 0; i < AUTHORIZED_UID_COUNT; i++) {
    bool match = true;
    for (byte j = 0; j < 4; j++) {
      if (uid->uidByte[j] != AUTHORIZED_UIDS[i][j]) {
        match = false;
        break;
      }
// ... continues for members in the complete validated source ...

Library dependency installation

The MFRC522 library is not part of the standard built-in Arduino core, so install it explicitly with Arduino CLI.

arduino-cli lib install "MFRC522"

Build/Flash/Run commands

The following commands match the required Arduino CLI workflow for Arduino UNO R3.

1) Update board index

arduino-cli core update-index

2) Install the AVR core

arduino-cli core install arduino:avr

3) Install the RFID library

arduino-cli lib install "MFRC522"

4) Create the sketch folder

Example directory:

mkdir -p rfid-door-lock-relay

Save the sketch as:

rfid-door-lock-relay/rfid_door_lock_relay.ino

5) Compile

Run this from the parent directory of the sketch folder, or specify the full path.

arduino-cli compile --fqbn arduino:avr:uno rfid-door-lock-relay

6) Find your serial port

On Linux:

arduino-cli board list

On Windows, the port may appear like COM3, COM4, etc.
On Linux, something like /dev/ttyACM0 or /dev/ttyUSB0.
On macOS, something like /dev/cu.usbmodem....

7) Upload

Replace <PORT> with your actual detected port.

arduino-cli upload --fqbn arduino:avr:uno --port <PORT> rfid-door-lock-relay

8) Open serial monitor

arduino-cli monitor --port <PORT> --config baudrate=9600

Step-by-step Validation

This section helps you prove the prototype behaves as intended.

1. Validate the compile stage

Goal:
– Confirm the sketch syntax, board target, and installed library are correct.

Procedure:
1. Run:
bash
arduino-cli compile --fqbn arduino:avr:uno rfid-door-lock-relay

2. Expected result:
– Compilation completes successfully.
– No missing-library errors for MFRC522.h.

What this validates:
– The code is structurally valid for Arduino UNO.
– The library dependency is correctly installed.

What this does not validate:
– Wiring
– Card readability
– Relay polarity
– Physical lock compatibility

2. Validate idle startup behavior

Goal:
– Confirm the board boots and the relay starts in the locked/idle state.

Procedure:
1. Upload the sketch.
2. Open the serial monitor at 9600 baud.
3. Reset the board if needed.

Expected log pattern:

RFID Door Lock Relay Prototype
Present a card/tag to the MFRC522 reader.
Known 4-byte UIDs will activate the relay.

Expected hardware behavior:
– Relay should be idle, not continuously energized.
– Optional LED should be off.

If the relay is ON at idle:
– Your module may use opposite input logic.
– Change:
cpp
static const bool RELAY_ACTIVE_LOW = true;

to:
cpp
static const bool RELAY_ACTIVE_LOW = false;

– Recompile and upload again.

3. Discover your card UID

Goal:
– Read the actual UID of your RFID card or key fob.

Procedure:
1. Present a card to the reader.
2. Watch the serial monitor.

Expected output example:

Card detected. UID=93:4A:1C:7F
ACCESS DENIED - unauthorized card

Record the UID exactly.

Notes:
– The provided code example authorizes only 4-byte UIDs.
– Many common MIFARE cards work this way, but some tags may have different UID lengths.
– For a basic project, use a card with a 4-byte UID if possible.

4. Add your real authorized UID

Goal:
– Make your card unlock the relay.

Procedure:
1. Edit this section:
cpp
const byte AUTHORIZED_UIDS[][4] = {
{0xDE, 0xAD, 0xBE, 0xEF},
{0x12, 0x34, 0x56, 0x78}
};

2. Replace one line with your actual UID bytes.
Example, if the monitor showed 93:4A:1C:7F:
cpp
const byte AUTHORIZED_UIDS[][4] = {
{0x93, 0x4A, 0x1C, 0x7F}
};

3. Recompile and upload.

5. Validate successful access

Goal:
– Confirm that an authorized card activates the relay for the configured duration.

Procedure:
1. Present the authorized card once.
2. Watch the serial output.
3. Listen for relay click and observe optional LED.

Expected output:

Card detected. UID=93:4A:1C:7F
ACCESS GRANTED - relay ON for 3000 ms
Relay OFF - lock returned to idle state

Expected measurable behavior:
– Relay turns on once
– Relay remains active for about 3 seconds
– Relay returns to idle automatically

6. Validate denied access

Goal:
– Ensure an unlisted card does not unlock.

Procedure:
1. Present a different RFID card or fob.
2. Watch the serial monitor.

Expected output:

Card detected. UID=11:22:33:44
ACCESS DENIED - unauthorized card

Expected hardware behavior:
– No unlock relay action
– Optional LED remains off

7. Validate repeat-read handling

Goal:
– Confirm the same card held near the reader does not rapidly retrigger.

Procedure:
1. Hold an authorized card on the reader continuously.
2. Observe behavior for a few seconds.

Expected result:
– One unlock event should occur.
– The debounce window avoids repeated immediate re-triggering of the same read.

Troubleshooting

RFID reader is not detecting any card

Check:

  • MFRC522 powered from 3.3 V, not 5 V
  • SPI pins are exactly:
  • D10 -> SDA/SS
  • D11 -> MOSI
  • D12 -> MISO
  • D13 -> SCK
  • D9 -> RST
  • Card is compatible with MFRC522 reader frequency and type
  • Grounds are common between all modules

Symptom:
– No UID messages at all in Serial Monitor

Likely causes:
– Wrong SPI wiring
– Missing library not likely if code already uploaded
– Reader power issue
– Poor jumper contact

Relay stays on all the time

Likely cause:
– Relay module polarity is opposite your code setting

Fix:
– Change:
cpp
static const bool RELAY_ACTIVE_LOW = true;

to:
cpp
static const bool RELAY_ACTIVE_LOW = false;

– Recompile and upload

Authorized card still says denied

Check:

  • UID bytes copied correctly
  • Hex values include 0x
  • Card has 4-byte UID
  • You re-uploaded after editing

Serial output shows UID in lowercase or mixed style

That is fine as long as byte values are correct. The matching logic uses raw bytes, not the printed format.

Relay clicks but your lock or test load does not activate

Check the switching side:

  • Are you using COM and NO?
  • Is the external low-voltage supply present?
  • Is the load circuit complete through relay contacts?
  • Is the load within relay current/voltage rating?

Arduino resets when relay switches

Possible reasons:

  • USB power is weak
  • Relay module causes noise or supply dip
  • Wiring is too loose or too long

Try:

  • Shorter wires
  • Better 5 V supply for the relay module if appropriate
  • Ensure common ground is solid
  • Test with only relay module first, then add lock-side load

Improvements

Once the basic prototype works, these are useful next steps:

Add separate status LEDs

Use:
– Green LED for granted
– Red LED for denied

This improves usability without needing the Serial Monitor.

Add a buzzer

A short beep for valid access and a different beep pattern for denied access makes the system feel more realistic.

Add enrollment mode

Instead of hardcoding UIDs, you could:
– press a button,
– scan a master card,
– store new UIDs in EEPROM.

That would make the prototype more practical.

Add a door sensor

A magnetic reed switch can detect whether the door actually opened or remained open too long.

Improve security logic

This project checks only card UID. For a more realistic system, you could explore:
– sector authentication concepts,
– master/admin cards,
– anti-passback ideas,
– event logging to external storage.

Add a lockout timer

After several denied attempts, the system could ignore scans for 30 seconds and blink a warning LED.

Final Checklist

Use this checklist before calling the project finished:

  • [ ] Arduino UNO R3 is wired correctly
  • [ ] MFRC522 is powered from 3.3 V
  • [ ] Relay module is wired to D7, 5V, and GND
  • [ ] Common ground exists between Arduino, RFID module, and relay module
  • [ ] Sketch saved as rfid_door_lock_relay.ino
  • [ ] MFRC522 library installed with Arduino CLI
  • [ ] Project compiles with:
    bash
    arduino-cli compile --fqbn arduino:avr:uno rfid-door-lock-relay
  • [ ] Project uploads with:
    bash
    arduino-cli upload --fqbn arduino:avr:uno --port <PORT> rfid-door-lock-relay
  • [ ] Serial Monitor at 9600 baud shows startup text
  • [ ] Unknown card produces ACCESS DENIED
  • [ ] Authorized card produces ACCESS GRANTED
  • [ ] Relay activates for about 3000 ms
  • [ ] Relay returns to idle automatically
  • [ ] Low-voltage test load switches correctly through relay contacts
  • [ ] You understand this is an educational prototype, not a certified security system

With that, you have a practical RFID door-lock relay prototype that a beginner can actually assemble, test, and extend into a more realistic access-control project.

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.

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

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

Follow me:

Quick Quiz

Question 1: What is the content property value for the pseudo-element in '.prometeo-educational-note h3::before'?




Question 2: What is the border-left-color of the '.prometeo-safety-note' class?




Question 3: What is the margin-bottom value for '.prometeo-educational-note p:last-child'?




Question 4: What is the border-radius value specified for '.prometeo-educational-note'?




Question 5: What is the background color of the '.prometeo-educational-note code' element?