Practical case: IoT Mailbox Monitor with ESP32

Practical case: IoT Mailbox Monitor with ESP32 — ESP32 with reed switch, magnet and LiPo module for mailbox open alert

Objective and use case

What you’ll build: A battery-powered, highly efficient IoT mailbox monitor that remains in a micro-power deep sleep state, wakes up instantly when the mailbox door opens, transmits a Wi-Fi alert via an HTTP GET request, and immediately returns to sleep.

Why it matters / Use cases

  • Eliminates wasted trips: Users no longer need to physically check an empty mailbox in harsh weather or across long driveways.
  • Maximizes battery lifecycle: By utilizing the ESP32’s deep sleep capabilities (drawing ~10µA), the device minimizes active duty cycles, demonstrating crucial power-budgeting skills.
  • Event-driven architecture: Teaches the transition from continuous polling to hardware-interrupt-driven wakeups, a foundational concept in commercial sensor networks.
  • Security and access logging: The same architecture can be repurposed for monitoring restricted cabinets, safes, or perimeter gates without requiring hardwired power.

Expected outcome

  • The ESP32 enters a deep sleep state drawing minimal current (sub-15µA).
  • Opening the door triggers a hardware wake-up via the RTC (Real-Time Clock) GPIO within 500 milliseconds.
  • The device successfully connects to Wi-Fi, transmits the HTTP payload, and powers down in under 3 seconds to preserve battery life.

Audience: IoT Developers, Embedded Engineers; Level: Intermediate

Architecture/flow: Reed Switch (Hardware Interrupt) → ESP32 RTC Wake-up (<500ms latency) → Wi-Fi Connect → HTTP GET Request → Deep Sleep

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, 3 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

Warning regarding prototype limits: This project is intended strictly as an educational prototype.
* Battery Safety: This project involves Lithium Polymer (LiPo) batteries. Mishandling, overcharging, or short-circuiting LiPo batteries can result in fire or explosion. Always use a dedicated charge controller (like the TP4056 with built-in protection circuitry) and never connect a LiPo cell directly to the ESP32 without proper voltage regulation. Do not leave experimental battery circuits unattended while charging. Ensure your battery module includes over-discharge protection to prevent cell damage during long deployments.

Conceptual block diagram

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

Functional architecture

Local button

ESP32 BLE

Advertising packet

Status LED

Phone scanner

Conceptual flow: local configuration, BLE advertising and phone-side reading.

Validation path

Source code

PlatformIO build

Flash

Serial monitor

Conceptual summary of the tools used to check the published ESP32 project.

Prerequisites

Before starting this tutorial, ensure you have the following ready:
* A computer running Windows, macOS, or Linux.
* Visual Studio Code (VSCode) installed.
* The PlatformIO IDE extension installed within VSCode.
* Basic familiarity with C++ syntax and electronic circuits (pull-up/pull-down networks).
* A 2.4 GHz Wi-Fi network (ESP32 does not support 5 GHz Wi-Fi).
* A local HTTP server or a webhook endpoint to receive the alert.

Materials

For this project, use the following exact hardware configuration:
* ESP32 DevKitC: The core microcontroller board featuring the ESP-WROOM-32 module.
* Reed switch: A magnetic contact switch (normally open). When the magnet is near, the switch closes. When the magnet is removed (door opens), the switch opens.
* LiPo battery module: A 3.7V Lithium Polymer battery paired with a 5V boost/charge module (e.g., a TP4056-based power bank module) to safely supply 5V to the ESP32 DevKitC’s 5V (or VIN) pin.
* Passive Components:
* One 10kΩ resistor (used as a pull-down resistor for the reed switch).
* Accessories: Breadboard, jumper wires, and a micro-USB data cable.

Setup/Connection

Hardware Setup Notes

The ESP32 DevKitC requires specific drivers to communicate over USB. Depending on your exact board revision, it will use either a CP210x or CH34x USB-to-UART bridge. If your board is not recognized by your operating system or PlatformIO, download and install the official Silicon Labs CP210x or WCH CH340 drivers.

Circuit Logic

The core of this project is the hardware interrupt. The ESP32’s main processor is turned off during deep sleep. Only the Ultra-Low Power (ULP) coprocessor and the RTC memory/peripherals remain active. We will use the ext0 wake-up source, which allows an RTC GPIO to wake the main processor when a specific logic level is detected.

We will use GPIO 33 (an RTC-capable pin) for the reed switch.
* Mailbox Closed: The magnet is near the reed switch. The switch is closed, connecting GPIO 33 to 3.3V. The ESP32 sees a HIGH signal.
* Mailbox Open: The door opens, the magnet moves away, and the switch opens. The 10kΩ pull-down resistor forces GPIO 33 to LOW.
* Wake-up condition: We will configure the ESP32 to wake up when GPIO 33 goes LOW.

Wiring Table

Component Pin/Terminal ESP32 DevKitC Pin Notes
LiPo Battery Module 5V Output / VOUT 5V (or VIN) Provides power when USB is disconnected.
LiPo Battery Module GND / Ground GND Common ground.
Reed Switch Terminal 1 3V3 Provides 3.3V when switch is closed.
Reed Switch Terminal 2 GPIO 33 Signal pin for RTC wake-up.
10kΩ Resistor Leg 1 GPIO 33 Pull-down configuration.
10kΩ Resistor Leg 2 GND Pull-down configuration.

Note: Disconnect the LiPo battery module’s 5V line from the ESP32 when plugging the ESP32 into your computer via USB to avoid power contention.

Application Firmware

The following code handles the deep sleep transition, Wi-Fi connection, HTTP request, and hardware wake-up logic.

platformio.ini

Create a new PlatformIO project for the ESP32 DevKitC and replace the contents of platformio.ini with the following configuration:

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

src/main.cpp

Replace the contents of src/main.cpp with the following code. Update the network and webhook variables to match your environment before deploying.

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

#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>

// ==========================================
// Configuration
// ==========================================
const char* WIFI_SSID     = "IoT_Network";
const char* WIFI_PASSWORD = "SecurePassword123";
const char* WEBHOOK_URL   = "http://192.168.1.100/mailbox-alert";

// Pin Definitions
const gpio_num_t REED_SWITCH_PIN = GPIO_NUM_33; // Must be an RTC GPIO for ext0 wakeup

// Timeouts
const int WIFI_TIMEOUT_MS = 10000; // 10 seconds max to connect

// ==========================================
// Helper Functions
// ==========================================

void printWakeupReason() {
    esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();
    switch(wakeup_reason) {
        case ESP_SLEEP_WAKEUP_EXT0:     
            Serial.println("Wakeup caused by external signal using RTC_IO (Door Opened!)"); 
            break;
        case ESP_SLEEP_WAKEUP_TIMER:    
            Serial.println("Wakeup caused by timer"); 
            break;
        default:                        
            Serial.printf("Wakeup was not caused by deep sleep: %d\n", wakeup_reason); 
            break;
    }
}

void sendAlert() {
    Serial.print("Connecting to Wi-Fi: ");
    Serial.println(WIFI_SSID);

    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

    unsigned long startAttemptTime = millis();

    // Wait for connection with a timeout to prevent draining battery
    while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < WIFI_TIMEOUT_MS) {
        Serial.print(".");
        delay(500);
    }

    Serial.println();

    if (WiFi.status() != WL_CONNECTED) {
        Serial.println("Failed to connect to Wi-Fi. Going back to sleep.");
        return; // Abort and let the main loop put the device to sleep
    }
// ...

#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>

// ==========================================
// Configuration
// ==========================================
const char* WIFI_SSID     = "IoT_Network";
const char* WIFI_PASSWORD = "SecurePassword123";
const char* WEBHOOK_URL   = "http://192.168.1.100/mailbox-alert";

// Pin Definitions
const gpio_num_t REED_SWITCH_PIN = GPIO_NUM_33; // Must be an RTC GPIO for ext0 wakeup

// Timeouts
const int WIFI_TIMEOUT_MS = 10000; // 10 seconds max to connect

// ==========================================
// Helper Functions
// ==========================================

void printWakeupReason() {
    esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();
    switch(wakeup_reason) {
        case ESP_SLEEP_WAKEUP_EXT0:     
            Serial.println("Wakeup caused by external signal using RTC_IO (Door Opened!)"); 
            break;
        case ESP_SLEEP_WAKEUP_TIMER:    
            Serial.println("Wakeup caused by timer"); 
            break;
        default:                        
            Serial.printf("Wakeup was not caused by deep sleep: %d\n", wakeup_reason); 
            break;
    }
}

void sendAlert() {
    Serial.print("Connecting to Wi-Fi: ");
    Serial.println(WIFI_SSID);

    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

    unsigned long startAttemptTime = millis();

    // Wait for connection with a timeout to prevent draining battery
    while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < WIFI_TIMEOUT_MS) {
        Serial.print(".");
        delay(500);
    }

    Serial.println();

    if (WiFi.status() != WL_CONNECTED) {
        Serial.println("Failed to connect to Wi-Fi. Going back to sleep.");
        return; // Abort and let the main loop put the device to sleep
    }

    Serial.println("Wi-Fi connected!");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());

    // Send HTTP GET request
    if (WiFi.status() == WL_CONNECTED) {
        HTTPClient http;
        Serial.print("Sending alert to: ");
        Serial.println(WEBHOOK_URL);

        http.begin(WEBHOOK_URL);
        int httpResponseCode = http.GET();

        if (httpResponseCode > 0) {
            Serial.print("HTTP Response code: ");
            Serial.println(httpResponseCode);
        } else {
            Serial.print("Error code: ");
            Serial.println(httpResponseCode);
        }
        http.end();
    }

    // Disconnect Wi-Fi to save power before sleeping
    WiFi.disconnect(true);
    WiFi.mode(WIFI_OFF);
}

// ==========================================
// Main Setup and Loop
// ==========================================

void setup() {
    Serial.begin(115200);
    delay(1000); // Allow serial monitor to catch up

    Serial.println("\n--- Mailbox Monitor Booting ---");

    // Determine why the ESP32 woke up
    esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();
    printWakeupReason();

    // If woke up because the door opened (EXT0)
    if (wakeup_reason == ESP_SLEEP_WAKEUP_EXT0) {
        sendAlert();
    } else {
        // Normal boot (e.g., first power on or reset)
        Serial.println("Initial boot. System is armed and ready.");
    }

    // Configure Deep Sleep Wakeup
    // Door open = magnet away = switch open = pull-down resistor pulls to LOW (0).
    // Wake up when GPIO 33 goes LOW (0).
    esp_sleep_enable_ext0_wakeup(REED_SWITCH_PIN, 0); 

    Serial.println("Entering deep sleep now. Waiting for door to open...");
    Serial.flush(); 

    // Enter Deep Sleep
    esp_deep_sleep_start();
}

void loop() {
    // The loop is intentionally empty.
    // The ESP32 goes to sleep in setup() and resets entirely upon waking.
}

Build/Flash/Run commands

To deploy the code to your ESP32 DevKitC using PlatformIO, use the built-in terminal in VSCode.

Command Reference

Action Command Purpose
Compile Code pio run Compiles the C++ code and libraries without uploading.
Upload Firmware pio run --target upload Flashes the compiled binary to the ESP32 via USB.
Serial Monitor pio device monitor Opens the terminal to view Serial.print outputs.

Workflow

  1. Connect the ESP32 DevKitC to your computer using a data-capable micro-USB cable.
  2. Open the VSCode terminal in your project directory.
  3. Compile and upload the firmware by running:
    pio run --target upload
  4. Immediately open the serial monitor to observe the boot sequence:
    pio device monitor

Step-by-step Validation

Follow these grouped checkpoints to ensure your prototype functions correctly.

  1. Initial Boot & Arming
    • Action: Press the EN (Reset) button on the ESP32.
    • Expected observation: The Serial Monitor prints “Initial boot. System is armed and ready.” The monitor then prints “Entering deep sleep now.”
    • Pass condition: The device successfully enters deep sleep without immediately waking back up.
  2. Simulating Mailbox Closed
    • Action: Place the magnet directly next to the reed switch.
    • Expected observation: Nothing happens. The ESP32 remains in deep sleep.
    • Pass condition: The system ignores the closed state (GPIO 33 is HIGH).
  3. Triggering the Wake-up (Mailbox Opened)
    • Action: Quickly pull the magnet away from the reed switch.
    • Expected observation: The Serial Monitor prints “Wakeup caused by external signal using RTC_IO (Door Opened!)”.
    • Pass condition: The hardware interrupt successfully wakes the ESP32 from deep sleep.
  4. Network Connection and Transmission
    • Action: Wait 2 to 5 seconds while watching the Serial Monitor.
    • Expected observation: Monitor prints “Wi-Fi connected!”, followed by the IP address, and then “HTTP Response code: 200”.
    • Pass condition: The webhook endpoint registers the incoming GET request.
  5. Return to Sleep
    • Action: Observe the final serial outputs.
    • Expected observation: Monitor prints “Entering deep sleep now. Waiting for door to open…”
    • Pass condition: The ESP32 drops the Wi-Fi connection and powers down its main processor, ready for the next event.

Troubleshooting

If the system does not behave as expected, consult the table below.

Symptom Likely cause Fix
Code fails to upload (Timeout) ESP32 is not entering bootloader mode. Hold the BOOT button on the DevKitC while running the upload command, release when connecting begins.
Constant wake-up loops Reed switch logic is floating. Verify the 10kΩ pull-down resistor is securely connected between GPIO 33 and GND. Ensure the magnet is close enough to close the switch initially.
Wi-Fi connection fails / Times out Incorrect credentials or 5GHz network. Verify WIFI_SSID and WIFI_PASSWORD. Ensure your router is broadcasting a 2.4GHz network.
Brownout detector triggered Insufficient power during Wi-Fi transmission. The USB port or LiPo module cannot supply the ~300mA spike required for Wi-Fi. Try a better USB cable or ensure the LiPo battery is fully charged.
No output on Serial Monitor Baud rate mismatch. Ensure monitor_speed = 115200 is set in platformio.ini and matches Serial.begin(115200) in the code.

Improvements

Once the basic prototype is validated, consider these enhancements for a production-ready device:

  • Power Optimization:
    • Static IP: Assign a static IP address to the ESP32 instead of using DHCP. This can reduce Wi-Fi connection time from ~3 seconds to under 500ms, drastically saving battery life.
    • Hardware LDO replacement: Standard ESP32 DevKitC boards use an AMS1117 voltage regulator with a high quiescent current (~1.5mA). For true micro-power operation, transition to a custom board or a low-dropout regulator (LDO) like the HT7333.
  • Connectivity & Reliability:
    • MQTT Implementation: Replace the HTTP GET request with an MQTT publish via the PubSubClient library. MQTT has lower overhead and is better suited for IoT ecosystems.
    • Battery Voltage Monitoring: Wire the LiPo battery output to an ESP32 ADC pin (via a voltage divider) to transmit battery health alongside the open-door alert.

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 IoT device described in the article?




Question 2: How does the device transmit an alert when the mailbox door opens?




Question 3: What is the approximate current draw of the ESP32 while in its deep sleep state?




Question 4: What foundational concept in commercial sensor networks does this project teach?




Question 5: What component triggers the hardware wake-up when the door opens?




Question 6: What does the device do immediately after transmitting the alert?




Question 7: According to the expected outcome, what is the maximum current drawn during deep sleep?




Question 8: Within what timeframe does the hardware wake-up occur after the door opens?




Question 9: What is one of the main user benefits of this mailbox monitor?




Question 10: What is another potential use case for this same event-driven architecture?




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 Washing Machine Monitor

Practical case: ESP32 Washing Machine Monitor — hero

Objective and use case

What you’ll build: A non-invasive laundry machine monitor using an ESP32 and SW-420 vibration sensor that detects when a wash cycle finishes. It uses edge-based timeout logic to trigger a local buzzer/LED alarm and a WiFi HTTP alert with sub-second latency.

Why it matters / Use cases

  • Prevents forgotten laundry: Provides immediate notifications when the machine stops, preventing mildew and the need for re-washing.
  • Optimizes shared facilities: Reduces wait times and unnecessary trips to the laundry room in dorms or apartment buildings.
  • Non-invasive monitoring: Safely retrofits onto older “dumb” appliances by relying on external vibrations rather than high-voltage circuitry.
  • Edge-based state machine logic: Teaches debouncing and timeout logic to handle normal appliance pauses (e.g., 3-5 minute idle windows between wash, rinse, and spin cycles).

Expected outcome

  • A functional ESP32 prototype that reliably detects vibration states and filters out false stops.
  • Local audio-visual alerting via a piezo buzzer and status LED.
  • A WiFi-triggered HTTP notification delivered in < 500ms when the machine officially completes its cycle.

Audience: IoT hobbyists, electronics students, and home automation enthusiasts; Level: Beginner to Intermediate

Architecture/flow: SW-420 Sensor → ESP32 (Debounce & Timeout State Machine) → Local Piezo/LED & WiFi HTTP POST Alert

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, 3 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 a low-voltage educational prototype, not a certified product. Before powering the setup, verify the pinout of your exact ESP32 DevKitC board, keep GPIO signals within 3.3 V limits, never apply 5 V to ESP32 inputs, disconnect power before changing wiring, and use suitable external supplies for relays, motors or loads while sharing GND only when the schematic requires it.

Conceptual block diagram

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

Functional architecture

Water probe

ESP32 GPIO/ADC

Threshold logic

LED/buzzer

Wi-Fi alert

Conceptual flow: moisture detection, local decision and user alert.

Validation path

Source code

PlatformIO build

Flash

Serial monitor

Conceptual summary of the tools used to check the published ESP32 project.

Prerequisites

Before starting this project, ensure you have the following ready:
* Software Environment: Visual Studio Code (VS Code) with the PlatformIO IDE extension installed.
* Basic C++ Knowledge: Familiarity with variables, if/else statements, and the millis() function for non-blocking delays.
* Network Access: A 2.4GHz WiFi network with known SSID and password credentials.
* USB Drivers: The CP210x or CH34x USB-to-UART drivers installed on your computer (depending on your specific ESP32 DevKitC variant) to allow serial communication and flashing.


Materials

You will need the following exact components for this build:
* ESP32 DevKitC: The core microcontroller, providing both GPIO logic and built-in 2.4GHz WiFi capabilities.
* SW-420 vibration sensor: A digital vibration module featuring an LM393 voltage comparator and a built-in potentiometer for threshold adjustment.
* Piezo buzzer: An active piezo buzzer module (sounds continuously when provided a HIGH digital signal).
* Status LED: A standard 5mm LED (any color, e.g., blue or green) paired with a 220Ω current-limiting resistor.
* Prototyping supplies: A standard breadboard, various male-to-male and male-to-female jumper wires, and a micro-USB cable for programming and power.


Setup/Connection

The SW-420 sensor outputs a digital signal: it remains LOW when still, and pulses HIGH when vibration exceeds the threshold set by its onboard potentiometer. The ESP32 will read this digital pin.

Make the connections according to the following mapping. Ensure your ESP32 is disconnected from USB power while wiring.

Component Component Pin ESP32 DevKitC Pin Notes
SW-420 Sensor VCC 3V3 Powers the LM393 comparator.
SW-420 Sensor GND GND Common ground.
SW-420 Sensor DO (Digital Out) GPIO 13 Sends HIGH pulses during vibration.
Piezo Buzzer VCC / + GPIO 14 Driven HIGH to sound the alarm.
Piezo Buzzer GND / – GND Common ground.
Status LED Anode (Long leg) GPIO 27 Connect via a 220Ω resistor.
Status LED Cathode (Short leg) GND Common ground.

Note on Buzzer Current: Standard active piezo buzzers draw around 10-30mA, which is safe to drive directly from an ESP32 GPIO pin (max 40mA per pin). If you are using a larger siren or high-power buzzer, you must use a switching transistor (like a 2N2222) or a relay module.


Validated Code

The following code implements a non-blocking state machine. To test the logic quickly, the cycle timeout is set to 15 seconds. In a real-world deployment, you would increase this to 3-5 minutes to account for the soaking/draining pauses in a washing machine cycle.

Create a new PlatformIO project for the esp32dev board, and replace the contents of the generated files with the code below.

platformio.ini

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

src/main.cpp

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

#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>

// ---------------------------------------------------------
// Configuration: WiFi & Webhook
// ---------------------------------------------------------
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";

// For demonstration, we use httpbin.org to echo the GET request.
// In a real application, replace this with an IFTTT Webhook, 
// Home Assistant endpoint, or custom API URL.
const char* WEBHOOK_URL = "http://httpbin.org/get?laundry=finished";

// ---------------------------------------------------------
// Configuration: Hardware Pins
// ---------------------------------------------------------
const int SW420_PIN = 13;
const int BUZZER_PIN = 14;
const int LED_PIN = 27;

// ---------------------------------------------------------
// Configuration: Timing & State Machine
// ---------------------------------------------------------
// Time required without vibration to consider the cycle "finished".
// Set to 15 seconds (15000ms) for testing. 
// For real laundry machines, use 180000ms (3 minutes) or more.
const unsigned long CYCLE_TIMEOUT_MS = 15000; 

enum MachineState {
    STATE_IDLE,
    STATE_RUNNING,
    STATE_FINISHED
};

MachineState currentState = STATE_IDLE;
unsigned long lastVibrationTime = 0;
unsigned long lastAlertToggleTime = 0;
bool alertToggleState = false;

// Function prototypes
void connectToWiFi();
void sendWiFiAlert();
void handleAlertHardware();

void setup() {
    Serial.begin(115200);
    delay(1000);
    Serial.println("\n--- Laundry Vibration WiFi Alert ---");

    // Initialize pins
    pinMode(SW420_PIN, INPUT);
    pinMode(BUZZER_PIN, OUTPUT);
    pinMode(LED_PIN, OUTPUT);

    // Ensure outputs are off initially
    digitalWrite(BUZZER_PIN, LOW);
    digitalWrite(LED_PIN, LOW);

    connectToWiFi();

    Serial.println("System initialized. Waiting for vibration...");
}

void loop() {
    unsigned long currentMillis = millis();
    int vibrationDetected = digitalRead(SW420_PIN);

    // 1. Read Sensor & Update Timers
    if (vibrationDetected == HIGH) {
        lastVibrationTime = currentMillis;

        // If we were idle or already finished, a new vibration means a cycle is starting/restarting
        if (currentState == STATE_IDLE || currentState == STATE_FINISHED) {
            currentState = STATE_RUNNING;
            Serial.println("STATUS: Machine is now RUNNING.");
            // Ensure alert hardware is turned off when returning to running
            digitalWrite(BUZZER_PIN, LOW);
            digitalWrite(LED_PIN, HIGH); // Solid LED indicates running
        }
    }
// ...

#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>

// ---------------------------------------------------------
// Configuration: WiFi & Webhook
// ---------------------------------------------------------
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";

// For demonstration, we use httpbin.org to echo the GET request.
// In a real application, replace this with an IFTTT Webhook, 
// Home Assistant endpoint, or custom API URL.
const char* WEBHOOK_URL = "http://httpbin.org/get?laundry=finished";

// ---------------------------------------------------------
// Configuration: Hardware Pins
// ---------------------------------------------------------
const int SW420_PIN = 13;
const int BUZZER_PIN = 14;
const int LED_PIN = 27;

// ---------------------------------------------------------
// Configuration: Timing & State Machine
// ---------------------------------------------------------
// Time required without vibration to consider the cycle "finished".
// Set to 15 seconds (15000ms) for testing. 
// For real laundry machines, use 180000ms (3 minutes) or more.
const unsigned long CYCLE_TIMEOUT_MS = 15000; 

enum MachineState {
    STATE_IDLE,
    STATE_RUNNING,
    STATE_FINISHED
};

MachineState currentState = STATE_IDLE;
unsigned long lastVibrationTime = 0;
unsigned long lastAlertToggleTime = 0;
bool alertToggleState = false;

// Function prototypes
void connectToWiFi();
void sendWiFiAlert();
void handleAlertHardware();

void setup() {
    Serial.begin(115200);
    delay(1000);
    Serial.println("\n--- Laundry Vibration WiFi Alert ---");

    // Initialize pins
    pinMode(SW420_PIN, INPUT);
    pinMode(BUZZER_PIN, OUTPUT);
    pinMode(LED_PIN, OUTPUT);

    // Ensure outputs are off initially
    digitalWrite(BUZZER_PIN, LOW);
    digitalWrite(LED_PIN, LOW);

    connectToWiFi();

    Serial.println("System initialized. Waiting for vibration...");
}

void loop() {
    unsigned long currentMillis = millis();
    int vibrationDetected = digitalRead(SW420_PIN);

    // 1. Read Sensor & Update Timers
    if (vibrationDetected == HIGH) {
        lastVibrationTime = currentMillis;

        // If we were idle or already finished, a new vibration means a cycle is starting/restarting
        if (currentState == STATE_IDLE || currentState == STATE_FINISHED) {
            currentState = STATE_RUNNING;
            Serial.println("STATUS: Machine is now RUNNING.");
            // Ensure alert hardware is turned off when returning to running
            digitalWrite(BUZZER_PIN, LOW);
            digitalWrite(LED_PIN, HIGH); // Solid LED indicates running
        }
    }

    // 2. Evaluate State Machine
    if (currentState == STATE_RUNNING) {
        // Check if the timeout has elapsed since the last detected vibration
        if (currentMillis - lastVibrationTime > CYCLE_TIMEOUT_MS) {
            currentState = STATE_FINISHED;
            Serial.println("STATUS: Cycle FINISHED! Triggering alerts.");
            digitalWrite(LED_PIN, LOW); // Turn off solid LED
            sendWiFiAlert();
        }
    }

    // 3. Handle Hardware Outputs based on State
    if (currentState == STATE_FINISHED) {
        handleAlertHardware();
    } else if (currentState == STATE_IDLE) {
        // Optional: Pulse LED slowly to show system is alive but idle
        digitalWrite(LED_PIN, (currentMillis / 1000) % 2 == 0);
    }

    // Small delay to prevent tight-loop debouncing issues
    delay(50);
}

// ---------------------------------------------------------
// Helper Functions
// ---------------------------------------------------------

void connectToWiFi() {
    Serial.print("Connecting to WiFi: ");
    Serial.println(WIFI_SSID);

    WiFi.mode(WIFI_STA);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

    int attempts = 0;
    while (WiFi.status() != WL_CONNECTED && attempts < 20) {
        delay(500);
        Serial.print(".");
        attempts++;
    }

    if (WiFi.status() == WL_CONNECTED) {
        Serial.println("\nWiFi connected.");
        Serial.print("IP Address: ");
        Serial.println(WiFi.localIP());
    } else {
        Serial.println("\nFailed to connect to WiFi. Continuing offline.");
    }
}

void sendWiFiAlert() {
    if (WiFi.status() == WL_CONNECTED) {
        HTTPClient http;
        Serial.print("Sending HTTP GET to: ");
        Serial.println(WEBHOOK_URL);

        http.begin(WEBHOOK_URL);
        int httpResponseCode = http.GET();

        if (httpResponseCode > 0) {
            Serial.print("HTTP Response code: ");
            Serial.println(httpResponseCode);
        } else {
            Serial.print("Error code: ");
            Serial.println(httpResponseCode);
        }
        http.end();
    } else {
        Serial.println("WiFi disconnected. Cannot send webhook alert.");
    }
}

void handleAlertHardware() {
    // Non-blocking toggle for Buzzer and LED (beep every 500ms)
    unsigned long currentMillis = millis();
    if (currentMillis - lastAlertToggleTime >= 500) {
        lastAlertToggleTime = currentMillis;
        alertToggleState = !alertToggleState;

        digitalWrite(BUZZER_PIN, alertToggleState ? HIGH : LOW);
        digitalWrite(LED_PIN, alertToggleState ? HIGH : LOW);
    }
}


Build/Flash/Run commands

Use the PlatformIO Core CLI to compile, upload, and monitor the project. Open the integrated terminal in VS Code and execute the following commands.

Command Purpose
pio run Compiles the C++ source code and checks for syntax errors.
pio run --target upload Flashes the compiled firmware to the connected ESP32 over USB.
pio device monitor Opens the serial monitor to view the ESP32’s console output.

Execution Workflow:
1. Update WIFI_SSID and WIFI_PASSWORD in src/main.cpp with your actual network credentials.
2. Connect the ESP32 DevKitC to your computer via micro-USB.
3. Run pio run to verify the code compiles successfully.
4. Run pio run --target upload to flash the device. (If the upload fails to start, press and hold the “BOOT” button on the ESP32 until the flashing progress percentage appears).
5. Run pio device monitor to observe the boot sequence, WiFi connection, and sensor states.


Step-by-step Validation

Follow these checkpoints to ensure your prototype functions correctly.

  1. Boot and WiFi Connection
    • Observation: Watch the serial monitor after resetting the ESP32.
    • Pass condition: The console displays “Connecting to WiFi…”, followed by “WiFi connected.” and an assigned IP address.
  2. Idle State Verification
    • Observation: Leave the sensor completely still on your desk.
    • Pass condition: The serial monitor says “System initialized. Waiting for vibration…” and the status LED blinks slowly (1Hz). No buzzer sounds.
  3. Vibration Detection (Running State)
    • Observation: Tap the SW-420 sensor gently with your finger or tap the table it rests on.
    • Pass condition: The serial monitor immediately prints “STATUS: Machine is now RUNNING.” The status LED turns solid ON.
  4. Cycle Completion (Timeout & Alert)
    • Observation: Stop tapping and wait for exactly 15 seconds (the CYCLE_TIMEOUT_MS).
    • Pass condition: The serial monitor prints “STATUS: Cycle FINISHED! Triggering alerts.” The LED and buzzer begin pulsing on and off every 500ms.
  5. Webhook/Network Verification
    • Observation: Immediately after the timeout, observe the HTTP output in the serial monitor.
    • Pass condition: The console prints “Sending HTTP GET…” followed by “HTTP Response code: 200”. This confirms the ESP32 successfully reached the external server.

Troubleshooting

Symptom Likely cause Fix
WiFi fails to connect Incorrect credentials or 5GHz network. Verify SSID/Password. Ensure your router is broadcasting a 2.4GHz band (ESP32 does not support 5GHz).
Sensor always triggers (No timeout) SW-420 sensitivity is set too high. Turn the blue potentiometer on the SW-420 module counter-clockwise to reduce sensitivity until the onboard green LED turns off when still.
Sensor never triggers SW-420 sensitivity is set too low. Turn the potentiometer clockwise until it triggers easily upon tapping the table.
Buzzer does not sound Polarity reversed or wrong pin. Ensure the longer leg (or + mark) of the buzzer goes to GPIO 14, and the other to GND.
HTTP Response code is -1 DNS failure or lack of internet. Verify the ESP32 is connected to a network with active internet access, not just a local router without WAN.

Improvements

Once the basic prototype is working, consider these enhancements for a permanent deployment:

Hardware Robustness:
* Enclosure and Mounting: Place the ESP32 in a 3D-printed case. Attach neodymium magnets to the back of the case so it can snap securely onto the metal chassis of the washing machine, ensuring excellent vibration transfer to the SW-420.
* Passive Buzzer / Audio: Swap the active buzzer for a passive buzzer. You can then use the ESP32’s PWM capabilities (ledcWrite) to play a pleasant melody (like a classic appliance chime) instead of a harsh beep.

Software and Logic:
* Realistic Timeouts: Increase CYCLE_TIMEOUT_MS to 180,000 (3 minutes) to account for the soaking and draining phases where a washing machine is temporarily completely still.
* MQTT Integration: Replace the basic HTTP GET request with an MQTT client (using the PubSubClient library) to integrate seamlessly with Home Assistant, Node-RED, or OpenHAB.
* Deep Sleep / Low Power: If running on a LiPo battery, modify the code to enter ESP32 Deep Sleep. You can use the SW-420 pin as an external wake

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 main purpose of the project described in the article?




Question 2: Which microcontroller is used in this project?




Question 3: What type of sensor is the SW-420 used in this build?




Question 4: Why is the timeout logic specifically needed in this project?




Question 5: What is the typical duration of the idle windows (pauses) mentioned in the text?




Question 6: How does the system notify the user locally when the cycle finishes?




Question 7: What is the expected latency for the WiFi-triggered HTTP notification?




Question 8: Why is this monitoring method considered 'non-invasive'?




Question 9: What is one of the benefits of using this monitor in shared facilities?




Question 10: What problem does this monitor prevent by providing immediate notifications?




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 RFID Clock-In to SD Card

Practical case: Arduino UNO RFID Clock-In to SD Card — hero

Objective and use case

What you’ll build: A standalone hardware prototype that reads passive RFID tags, retrieves the current time from a precision real-time clock (RTC), and logs the event (UID and timestamp) to a CSV file on a microSD card.

Why it matters / Use cases

  • Student Attendance Tracking: Automates classroom check-ins via ID card taps, eliminating manual roll calls.
  • Lab Equipment Access Logging: Provides an offline audit trail of exactly who used a specific piece of machinery and at what time.
  • Employee Time Clock: Acts as a basic punch-in/punch-out system for small workshops where network connectivity is unavailable.
  • Security Auditing: Demonstrates the foundational principles of physical access control systems by logging entry attempts at a door or gate.

Expected outcome

  • Successful initialization of shared SPI bus components (microSD and RFID) and I2C components (RTC).
  • Serial monitor output displaying detected card UIDs and timestamps in real-time (< 100ms read latency).
  • Creation of an ATTEND.CSV file on the microSD card containing properly formatted, comma-separated log entries.

Audience: Embedded systems developers and makers; Level: Intermediate

Architecture/flow: RFID Tag Tap → SPI RFID Reader → Microcontroller → I2C RTC Timestamp Fetch → SPI microSD CSV Append

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, 5 tables and 2 code blocks detected before publication.
  • Checked code: 2 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 builds an educational prototype intended for learning hardware integration, SPI bus sharing, and data logging. It is not a secure access control system. The MIFARE Classic tags commonly used with the MFRC522 have well-documented cryptographic vulnerabilities and

Conceptual block diagram

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

Functional architecture

RFID Tag Tap

SPI RFID Reader

Microcontroller

I2C RTC Timestamp Fetch

SPI microSD CSV Append

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 need:
* Basic understanding of C++ programming and Arduino sketch structure (setup() and loop()).
* Familiarity with serial communication and reading serial monitor outputs.
* A computer with the arduino-cli (Arduino Command Line Interface) installed and added to your system path.
* A microSD card (32GB or smaller) formatted to FAT32.
* A USB Type-A to Type-B cable for programming and powering the Arduino UNO.

Materials

  • Arduino UNO R3 (ATmega328P) + MFRC522 RFID module + microSD SPI module + DS3231 RTC (Exact target hardware).
  • Passive RFID Tags or Cards (13.56 MHz, compatible with MFRC522, typically MIFARE Classic).
  • Breadboard and premium male-to-male and male-to-female jumper wires.
  • MicroSD card (formatted to FAT32).

Setup/Connection

This project requires careful wiring because two different modules (the MFRC522 and the microSD module) must share the Arduino’s single hardware SPI bus. The SPI bus uses shared lines for data (MOSI, MISO) and clock (SCK), but requires unique Chip Select (CS) pins for each device. The DS3231 RTC uses the I2C bus, which operates on separate pins.

Power Distribution Warning

  • MFRC522: Must be powered by 3.3V. Applying 5V to the VCC pin of the MFRC522 will damage it.
  • MicroSD Module: Most standard Arduino microSD modules have an onboard voltage regulator and level shifter. These should be powered by 5V.
  • DS3231: Can safely operate at 5V.

Wiring Tables

1. MFRC522 RFID Module (SPI Bus 1 – 3.3V Logic)

MFRC522 Pin Arduino UNO R3 Pin Function / Note
3.3V 3.3V CRITICAL: Do not connect to 5V
RST Pin 9 Reset control
GND GND Common Ground
IRQ Unconnected Not used in this polling implementation
MISO Pin 12 Master In Slave Out (Shared SPI)
MOSI Pin 11 Master Out Slave In (Shared SPI)
SCK Pin 13 Serial Clock (Shared SPI)
SDA (CS) Pin 10 Chip Select for RFID

2. MicroSD Card Module (SPI Bus 2 – 5V Logic)

MicroSD Pin Arduino UNO R3 Pin Function / Note
VCC 5V Power for onboard regulator
GND GND Common Ground
MISO Pin 12 Master In Slave Out (Shared SPI)
MOSI Pin 11 Master Out Slave In (Shared SPI)
SCK Pin 13 Serial Clock (Shared SPI)
CS Pin 4 Chip Select for SD Card

3. DS3231 RTC Module (I2C Bus)

DS3231 Pin Arduino UNO R3 Pin Function / Note
VCC 5V Power
GND GND Common Ground
SDA Pin A4 I2C Data
SCL Pin A5 I2C Clock

Validated Code

The project uses two separate code files. The first is a utility sketch to set the current date and time on your DS3231 RTC. The second is the main attendance logger application.

Utility Sketch: set_rtc_time.ino

Run this sketch once to program the RTC with your computer’s current compile time.

/*
 * Utility: Set DS3231 RTC Time
 * This sketch sets the RTC to the date & time the sketch was compiled.
 */

#include <Wire.h>
#include <RTClib.h>

RTC_DS3231 rtc;

void setup() {
  Serial.begin(9600);
  while (!Serial) { delay(10); } // Wait for serial console

  Serial.println("Initializing RTC...");

  if (!rtc.begin()) {
    Serial.println("Couldn't find RTC. Check wiring.");
    while (1) { delay(10); } // Halt
  }

  if (rtc.lostPower()) {
    Serial.println("RTC lost power, let's set the time!");
  }

  // Set the RTC to the date & time this sketch was compiled
  rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));

  Serial.println("RTC time has been successfully updated!");
  Serial.print("Current time set to: ");

  DateTime now = rtc.now();
  Serial.print(now.year(), DEC);
  Serial.print('/');
  Serial.print(now.month(), DEC);
  Serial.print('/');
  Serial.print(now.day(), DEC);
  Serial.print(" ");
  Serial.print(now.hour(), DEC);
  Serial.print(':');
  Serial.print(now.minute(), DEC);
  Serial.print(':');
  Serial.println(now.second(), DEC);

  Serial.println("You can now flash the main attendance logger sketch.");
}

void loop() {
  // Nothing to do here
}

Main Application: attendance_logger.ino

This is the primary firmware for the prototype. It initializes the shared SPI bus, manages the Chip Select pins, polls for RFID cards, and appends records to the SD card.

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

/*
 * Project: RFID Attendance SD Logger
 * Target: Arduino UNO R3 + MFRC522 + MicroSD + DS3231
 * Description: Reads RFID tag UIDs, fetches timestamp from RTC, 
 *              and logs the data to ATTEND.CSV on the SD card.
 */

#include <SPI.h>
#include <MFRC522.h>
#include <SD.h>
#include <Wire.h>
#include <RTClib.h>

// --- Pin Definitions ---
#define SD_CS_PIN    4
#define RFID_CS_PIN  10
#define RFID_RST_PIN 9

// --- Object Instantiation ---
MFRC522 mfrc522(RFID_CS_PIN, RFID_RST_PIN);
RTC_DS3231 rtc;

// --- Global Variables ---
const char* logFileName = "ATTEND.CSV";

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

  Serial.println(F("--- RFID Attendance Logger Initialization ---"));

  // 1. Initialize SPI Bus
  SPI.begin();

  // 2. Initialize DS3231 RTC (I2C)
  if (!rtc.begin()) {
    Serial.println(F("ERROR: Couldn't find RTC."));
    while (1) { delay(10); }
  }
  if (rtc.lostPower()) {
    Serial.println(F("WARNING: RTC lost power. Time may be inaccurate."));
  } else {
    Serial.println(F("RTC initialized successfully."));
  }

  // 3. Initialize MicroSD Card (SPI)
  // Disable RFID SPI temporarily to ensure clean SD init
  pinMode(RFID_CS_PIN, OUTPUT);
  digitalWrite(RFID_CS_PIN, HIGH); 

  Serial.print(F("Initializing SD card..."));
  if (!SD.begin(SD_CS_PIN)) {
    Serial.println(F("ERROR: SD card initialization failed!"));
    Serial.println(F("Check formatting (FAT32), wiring, and CS pin."));
    while (1) { delay(10); } // Halt if SD fails
  }
  Serial.println(F("SD card initialized."));

  // Write CSV Header if file doesn't exist
  if (!SD.exists(logFileName)) {
    File dataFile = SD.open(logFileName, FILE_WRITE);
    if (dataFile) {
      dataFile.println(F("Timestamp,UID"));
      dataFile.close();
      Serial.println(F("Created new ATTEND.CSV with headers."));
    } else {
      Serial.println(F("ERROR: Could not create file on SD card."));
    }
// ...

/*
 * Project: RFID Attendance SD Logger
 * Target: Arduino UNO R3 + MFRC522 + MicroSD + DS3231
 * Description: Reads RFID tag UIDs, fetches timestamp from RTC, 
 *              and logs the data to ATTEND.CSV on the SD card.
 */

#include <SPI.h>
#include <MFRC522.h>
#include <SD.h>
#include <Wire.h>
#include <RTClib.h>

// --- Pin Definitions ---
#define SD_CS_PIN    4
#define RFID_CS_PIN  10
#define RFID_RST_PIN 9

// --- Object Instantiation ---
MFRC522 mfrc522(RFID_CS_PIN, RFID_RST_PIN);
RTC_DS3231 rtc;

// --- Global Variables ---
const char* logFileName = "ATTEND.CSV";

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

  Serial.println(F("--- RFID Attendance Logger Initialization ---"));

  // 1. Initialize SPI Bus
  SPI.begin();

  // 2. Initialize DS3231 RTC (I2C)
  if (!rtc.begin()) {
    Serial.println(F("ERROR: Couldn't find RTC."));
    while (1) { delay(10); }
  }
  if (rtc.lostPower()) {
    Serial.println(F("WARNING: RTC lost power. Time may be inaccurate."));
  } else {
    Serial.println(F("RTC initialized successfully."));
  }

  // 3. Initialize MicroSD Card (SPI)
  // Disable RFID SPI temporarily to ensure clean SD init
  pinMode(RFID_CS_PIN, OUTPUT);
  digitalWrite(RFID_CS_PIN, HIGH); 

  Serial.print(F("Initializing SD card..."));
  if (!SD.begin(SD_CS_PIN)) {
    Serial.println(F("ERROR: SD card initialization failed!"));
    Serial.println(F("Check formatting (FAT32), wiring, and CS pin."));
    while (1) { delay(10); } // Halt if SD fails
  }
  Serial.println(F("SD card initialized."));

  // Write CSV Header if file doesn't exist
  if (!SD.exists(logFileName)) {
    File dataFile = SD.open(logFileName, FILE_WRITE);
    if (dataFile) {
      dataFile.println(F("Timestamp,UID"));
      dataFile.close();
      Serial.println(F("Created new ATTEND.CSV with headers."));
    } else {
      Serial.println(F("ERROR: Could not create file on SD card."));
    }
  }

  // 4. Initialize MFRC522 RFID (SPI)
  mfrc522.PCD_Init();
  // Optional: Increase antenna gain if tags are hard to read
  // mfrc522.PCD_SetAntennaGain(mfrc522.RxGain_max);
  Serial.println(F("MFRC522 initialized successfully."));

  Serial.println(F("--- System Ready. Waiting for RFID tags ---"));
}

void loop() {
  // Look for new RFID cards
  if (!mfrc522.PICC_IsNewCardPresent()) {
    return;
  }

  // Select one of the cards
  if (!mfrc522.PICC_ReadCardSerial()) {
    return;
  }

  // Get Current Time from RTC
  DateTime now = rtc.now();

  // Format Timestamp: YYYY-MM-DD HH:MM:SS
  char timeBuffer[20];
  snprintf(timeBuffer, sizeof(timeBuffer), "%04d-%02d-%02d %02d:%02d:%02d",
           now.year(), now.month(), now.day(),
           now.hour(), now.minute(), now.second());

  // Format UID to Hex String
  String uidString = "";
  for (byte i = 0; i < mfrc522.uid.size; i++) {
    if (mfrc522.uid.uidByte[i] < 0x10) {
      uidString += "0";
    }
    uidString += String(mfrc522.uid.uidByte[i], HEX);
  }
  uidString.toUpperCase();

  // Print to Serial Monitor
  Serial.print(F("Scanned: "));
  Serial.print(timeBuffer);
  Serial.print(F(" | UID: "));
  Serial.println(uidString);

  // Log to SD Card
  File dataFile = SD.open(logFileName, FILE_WRITE);
  if (dataFile) {
    dataFile.print(timeBuffer);
    dataFile.print(F(","));
    dataFile.println(uidString);
    dataFile.close();
    Serial.println(F(" -> Logged to SD card successfully."));
  } else {
    Serial.println(F(" -> ERROR: Failed to open ATTEND.CSV for writing."));
  }

  // Halt PICC to prevent reading the same card repeatedly in a single tap
  mfrc522.PICC_HaltA();
  // Stop encryption on PCD
  mfrc522.PCD_StopCrypto1();

  // Brief delay to prevent rapid-fire multiple reads
  delay(1000); 
}

Build/Flash/Run commands

Use the arduino-cli to compile and upload your code.

Command Reference Table

Task Command
Update core index arduino-cli core update-index
Install AVR core arduino-cli core install arduino:avr
Install dependencies arduino-cli lib install "MFRC522" "RTClib" "SD"
Compile sketch arduino-cli compile --fqbn arduino:avr:uno <Sketch_Folder>
Upload to UNO arduino-cli upload --fqbn arduino:avr:uno --port <PORT> <Sketch_Folder>
Serial Monitor arduino-cli monitor --port <PORT> --config baudrate=9600

Numbered Workflow

  1. Find your port: Connect the Arduino UNO to your computer. Run arduino-cli board list to identify the <PORT> (e.g., COM3 on Windows, /dev/ttyACM0 on Linux).
  2. Install Libraries: Ensure the required third-party libraries are installed:
    arduino-cli lib install "MFRC522" "RTClib" "SD"
  3. Set the RTC Time:
    • Save the first code block in a folder named set_rtc_time.
    • Compile: arduino-cli compile --fqbn arduino:avr:uno set_rtc_time
    • Upload: arduino-cli upload --fqbn arduino:avr:uno --port <PORT> set_rtc_time
    • Monitor: arduino-cli monitor --port <PORT> --config baudrate=9600 to verify the time was set.
  4. Flash the Main Logger:
    • Save the second code block in a folder named attendance_logger.
    • Compile: arduino-cli compile --fqbn arduino:avr:uno attendance_logger
    • Upload: arduino-cli upload --fqbn arduino:avr:uno --port <PORT> attendance_logger
    • Monitor: arduino-cli monitor --port <PORT> --config baudrate=9600 to view the live system.

Step‑by‑step Validation

Use these checkpoints while monitoring the serial output to ensure your prototype is fully functional.

  1. Checkpoint: RTC Initialization
    • Action: Reset the Arduino and watch the serial monitor.
    • Expected observation: The monitor prints “RTC initialized successfully.”
    • Pass condition: The system does not halt with “ERROR: Couldn’t find RTC.”
  2. Checkpoint: SD Card Mounting
    • Action: Continue watching the serial monitor during boot.
    • Expected observation: The monitor prints “Initializing SD card…” followed by “SD card initialized.”
    • Pass condition: The system successfully mounts the FAT32 filesystem and does not halt.
  3. Checkpoint: RFID Module Initialization
    • Action: Observe the final boot messages.
    • Expected observation: The monitor prints “MFRC522 initialized successfully.” and “— System Ready. Waiting for RFID tags —“.
    • Pass condition: Both SPI devices (SD and RFID) have initialized without causing bus conflicts.
  4. Checkpoint: Tag Scanning and Logging
    • Action: Tap a 13.56 MHz RFID tag against the MFRC522 antenna.
    • Expected observation: The serial monitor outputs “Scanned: [Date/Time] | UID: [Hex String]” followed by ” -> Logged to SD card successfully.”
    • Pass condition: The UID matches your card, the timestamp is accurate, and the SD write reports success.
  5. Checkpoint: CSV Data Verification
    • Action: Power down the Arduino, remove the microSD card, and read it on your computer.
    • Expected observation: A file named ATTEND.CSV exists. Opening it reveals a header row (Timestamp,UID) and a row for each card tap.
    • Pass condition: The data is properly comma-separated and human-readable in a spreadsheet application.

Troubleshooting

Symptom Likely cause Fix
Couldn't find RTC I2C wiring issue. Verify SDA is on A4 and SCL is on A5. Ensure the module has 5V and GND.
SD card initialization failed! Incorrect formatting or CS pin. Ensure the SD card is formatted to FAT32 (not exFAT or NTFS). Verify CS is connected to Pin 4.
SD fails only when RFID is connected SPI MISO conflict (Cheap SD modules). Some SD modules do not release the MISO line when their CS is HIGH. Try powering the SD module from a separate 5V source or add a tri-state buffer to the SD MISO line.
RFID does not detect cards Power supply or SPI wiring. Ensure MFRC522 VCC is 3.3V, NOT 5V. Verify MOSI (11), MISO (12), SCK (13), SDA (10), and RST (9).
Timestamp is resetting to 2000-01-01 RTC battery depleted. Replace the CR2032 battery on the DS3231 module and re-run the set_rtc_time sketch.

Improvements

Once the basic logger is working, consider these thematic enhancements for a more robust prototype:

  • User Feedback Mechanisms:
    • Add a piezoelectric buzzer to provide an audible “beep” upon a successful scan and log.
    • Integrate a dual-color LED (Green for successful log, Red for SD card error) so the device can be used without a serial monitor attached.
  • Data Management & Integrity:
    • Implement a daily file rotation system (e.g., LOG_1024.CSV for October 24) to prevent a single CSV file from becoming too large to parse quickly.
    • Add a known-UID database in the Arduino’s flash memory (using PROGMEM) to reject unknown cards and only log authorized users.
  • Power and Deployment:
    • Add a sleep mode routine to power down the SPI peripherals and put the ATmega328P to sleep when no card is present, waking via a hardware interrupt (if using a different RFID module that supports IRQ wake).

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




Question 2: Which component provides the current time for the log events?




Question 3: What communication protocol is shared by the microSD and RFID components?




Question 4: What communication protocol is used for the real-time clock (RTC)?




Question 5: What is the name of the file created on the microSD card to store the log entries?




Question 6: What is the expected read latency for displaying detected card UIDs and timestamps?




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




Question 8: What specific data does the prototype log to the CSV file?




Question 9: How does the prototype assist with Lab Equipment Access Logging?




Question 10: What foundational principles does the Security Auditing use case demonstrate?




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: greenhouse vent with Arduino UNO

Practical case: greenhouse vent with Arduino UNO — hero

Objective and use case

What you’ll build: You will build a standalone automated greenhouse vent controller that dynamically adjusts a physical ventilation flap via a servo motor based on ambient temperature. The system incorporates a manual hardware override and hysteresis logic to prevent mechanical oscillation.

Why it matters / Use cases

  • Agricultural Automation: Regulate small-scale greenhouses autonomously, eliminating the need for constant human monitoring to prevent plant stress.
  • Hysteresis Implementation: Utilize distinct upper and lower temperature thresholds (e.g., open flap at 26°C, close at 22°C) to stabilize the system and prevent servo wear from rapid toggling.
  • Safety Overrides: Provide a hardware-based manual bypass (via limit switch) for immediate control during emergencies or routine maintenance.

Expected outcome

  • A closed-loop system that drives a servo from 0° (closed) to 90° (open) with <50ms response latency when temperature thresholds are crossed.
  • Smooth, jitter-free mechanical operation at temperature boundaries due to programmed hysteresis.
  • Immediate mechanical response to the limit switch, preempting all automated sensor logic with near-zero latency.

Audience: Makers, agriculture tech students, and embedded developers; Level: Beginner to Intermediate

Architecture/flow: Temperature Sensor (Analog Input) ➔ Microcontroller (Hysteresis Logic & Hardware Interrupts) ➔ PWM Output ➔ Servo Motor Actuator

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 a low-voltage educational prototype, not a certified product. Before powering the setup, verify the wiring of your Arduino UNO R3, avoid shorting 5 V, GND or digital pins, disconnect power before changing connections, and use proper interface modules for relays, motors or external loads.

Prerequisites

To successfully complete this tutorial, you should have:
* A basic understanding of how to use the command line/terminal on your operating system.
* The Arduino CLI (Command Line Interface) installed on your computer.
* Basic familiarity with breadboarding and jumper wire connections.
* A standard USB Type-B cable to connect the Arduino UNO to your computer.

Materials

For this project, you must use EXACTLY this device model and component list:
* Microcontroller: Arduino UNO R3 (ATmega328P)
* Actuator: SG90 micro servo motor
* Sensor: LM35 analog temperature sensor (TO-92 package)
* Input Device: Limit switch (standard microswitch with a roller lever or basic push button)
* Accessories: 1x Solderless breadboard, assorted male-to-male jumper wires.

Note: No external resistors are required for the limit switch because we will utilize the ATmega328P’s internal pull-up resistors via software.

Setup/Connection

Proper wiring is critical for the stability of analog readings and servo movements. The LM35 provides a linear analog voltage output proportional to the temperature (10mV per degree Celsius). The SG90 servo is controlled via Pulse Width Modulation (PWM), and the limit switch uses a simple digital input.

Wiring Table

Component Pin / Wire Color Arduino UNO R3 Pin Function / Notes
LM35 Pin 1 (Left, flat face up) 5V Power supply for the temperature sensor.
LM35 Pin 2 (Middle) A0 Analog output signal (10mV/°C).
LM35 Pin 3 (Right) GND Ground reference.
SG90 Servo Red Wire 5V Power supply for the servo motor.
SG90 Servo Brown / Black Wire GND Ground reference.
SG90 Servo Orange / Yellow Wire D9 PWM signal to control servo angle.
Limit Switch COM (Common) GND Ground reference for the switch.
Limit Switch NO (Normally Open) D2 Digital input. Pulled HIGH internally; goes LOW when pressed.

Connection Instructions

  1. Power Distribution: Connect the 5V pin from the Arduino to the positive rail of your breadboard. Connect the GND pin from the Arduino to the negative rail.
  2. LM35 Sensor: Insert the LM35 into the breadboard. With the flat side facing you, connect the left pin to the 5V rail, the right pin to the GND rail, and the center pin directly to Arduino analog pin A0.
  3. SG90 Servo: Connect the servo’s power wires (Red to 5V, Brown/Black to GND) to the breadboard rails. Connect the signal wire (Orange/Yellow) to Arduino digital pin D9.
  4. Limit Switch: Connect the Common (COM) terminal of the limit switch to the GND rail. Connect the Normally Open (NO) terminal to Arduino digital pin D2.

Validated Code

The following section contains the complete, compilable Arduino sketch and a Bash shell script used to automate the build and upload process using Arduino CLI.

Arduino Sketch: greenhouse_vent.ino

Create a directory named greenhouse_vent and save this code inside it as greenhouse_vent.ino.

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

/*
 * Greenhouse Vent Servo Controller
 * Device: Arduino UNO R3 (ATmega328P) + SG90 servo + LM35 temperature sensor + limit switch
 * 
 * Description: Reads temperature from LM35. Opens vent (servo 90 deg) if temp >= 28C.
 * Closes vent (servo 0 deg) if temp <= 25C. 
 * A limit switch on D2 acts as a manual override to force the vent open.
 */

#include <Servo.h>

// Pin Definitions
const int lm35Pin = A0;
const int limitSwitchPin = 2;
const int servoPin = 9;

// Servo Object
Servo ventServo;

// Configuration Constants
const float TEMP_OPEN_THRESHOLD = 28.0;
const float TEMP_CLOSE_THRESHOLD = 25.0;
const int ANGLE_CLOSED = 0;
const int ANGLE_OPEN = 90;

// Timing Variables for non-blocking execution
unsigned long lastUpdateMillis = 0;
const unsigned long UPDATE_INTERVAL_MS = 1000;

// State Tracking
bool ventIsOpen = false;

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

  // Configure Pins
  // Internal pull-up ensures the pin reads HIGH when the switch is unpressed.
  // When pressed, the switch connects the pin to GND, reading LOW.
  pinMode(limitSwitchPin, INPUT_PULLUP);

  // Attach and initialize servo to closed position
  ventServo.attach(servoPin);
  ventServo.write(ANGLE_CLOSED);

  Serial.println("========================================");
  Serial.println("Greenhouse Vent Controller Initialized");
  Serial.println("========================================");
}
// ...

/*
 * Greenhouse Vent Servo Controller
 * Device: Arduino UNO R3 (ATmega328P) + SG90 servo + LM35 temperature sensor + limit switch
 * 
 * Description: Reads temperature from LM35. Opens vent (servo 90 deg) if temp >= 28C.
 * Closes vent (servo 0 deg) if temp <= 25C. 
 * A limit switch on D2 acts as a manual override to force the vent open.
 */

#include <Servo.h>

// Pin Definitions
const int lm35Pin = A0;
const int limitSwitchPin = 2;
const int servoPin = 9;

// Servo Object
Servo ventServo;

// Configuration Constants
const float TEMP_OPEN_THRESHOLD = 28.0;
const float TEMP_CLOSE_THRESHOLD = 25.0;
const int ANGLE_CLOSED = 0;
const int ANGLE_OPEN = 90;

// Timing Variables for non-blocking execution
unsigned long lastUpdateMillis = 0;
const unsigned long UPDATE_INTERVAL_MS = 1000;

// State Tracking
bool ventIsOpen = false;

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

  // Configure Pins
  // Internal pull-up ensures the pin reads HIGH when the switch is unpressed.
  // When pressed, the switch connects the pin to GND, reading LOW.
  pinMode(limitSwitchPin, INPUT_PULLUP);

  // Attach and initialize servo to closed position
  ventServo.attach(servoPin);
  ventServo.write(ANGLE_CLOSED);

  Serial.println("========================================");
  Serial.println("Greenhouse Vent Controller Initialized");
  Serial.println("========================================");
}

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

  // Execute control logic at defined intervals
  if (currentMillis - lastUpdateMillis >= UPDATE_INTERVAL_MS) {
    lastUpdateMillis = currentMillis;

    // 1. Read Manual Override Limit Switch
    // LOW means the switch is pressed (override active)
    bool overrideActive = (digitalRead(limitSwitchPin) == LOW);

    // 2. Read and Calculate Temperature from LM35
    int rawADC = analogRead(lm35Pin);

    // The Arduino UNO has a 10-bit ADC (0-1023) and operates at 5.0V.
    // Voltage = (ADC Value / 1024.0) * 5.0
    float voltage = rawADC * (5.0 / 1024.0);

    // LM35 outputs 10mV per degree Celsius (0.01V/C)
    // Temperature (C) = Voltage / 0.01 = Voltage * 100.0
    float temperatureC = voltage * 100.0;

    // 3. Determine Target Vent State
    if (overrideActive) {
      // Manual override forces the vent open
      ventIsOpen = true;
    } else {
      // Temperature-based hysteresis control
      if (temperatureC >= TEMP_OPEN_THRESHOLD) {
        ventIsOpen = true;
      } else if (temperatureC <= TEMP_CLOSE_THRESHOLD) {
        ventIsOpen = false;
      }
      // If temperature is between 25.0 and 28.0, ventIsOpen remains unchanged.
    }

    // 4. Actuate Servo
    if (ventIsOpen) {
      ventServo.write(ANGLE_OPEN);
    } else {
      ventServo.write(ANGLE_CLOSED);
    }

    // 5. Log System State
    Serial.print("Temp: ");
    Serial.print(temperatureC, 1);
    Serial.print(" C | Override: ");
    Serial.print(overrideActive ? "ACTIVE " : "STANDBY");
    Serial.print(" | Vent State: ");
    Serial.println(ventIsOpen ? "OPEN  (90 deg)" : "CLOSED (0 deg)");
  }
}

Automation Script: build_and_upload.sh

Save this file in the parent directory of greenhouse_vent (or adjust paths accordingly). This script ensures the Arduino AVR core is installed, compiles the code, and uploads it to the board.

#!/bin/bash

# Define the FQBN for Arduino UNO R3
FQBN="arduino:avr:uno"

# Define the serial port (Change this to match your system, e.g., /dev/ttyACM0 or COM3)
PORT="/dev/ttyACM0"

# Define the sketch directory
SKETCH_DIR="greenhouse_vent"

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

echo "Installing Arduino AVR core..."
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

    if [ $? -eq 0 ]; then
        echo "Upload complete! Open serial monitor at 9600 baud."
    else
        echo "Upload failed. Please check the PORT and connection."
    fi
else
    echo "Compilation failed. Please check the source code."
fi

Build/Flash/Run commands

To deploy the code to your Arduino UNO R3, you will use the Arduino CLI. Below are the exact commands used by the automation script, which you can also run manually.

Command Reference Table

Action 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 greenhouse_vent
Upload to board arduino-cli upload --fqbn arduino:avr:uno --port <PORT> greenhouse_vent
Monitor output arduino-cli monitor --port <PORT> --config baudrate=9600

Numbered Workflow

  1. Connect your Arduino UNO R3 to your computer via the USB cable.
  2. Identify your serial port. On Linux, this is typically /dev/ttyACM0 or /dev/ttyUSB0. On Windows, it will be a COM port like COM3. You can find it by running arduino-cli board list.
  3. Open your terminal and navigate to the directory containing your greenhouse_vent folder.
  4. Run the compilation command: arduino-cli compile --fqbn arduino:avr:uno greenhouse_vent.
  5. Run the upload command, replacing <PORT> with your actual port: arduino-cli upload --fqbn arduino:avr:uno --port /dev/ttyACM0 greenhouse_vent.
  6. Start the serial monitor to view the logs: arduino-cli monitor --port /dev/ttyACM0 --config baudrate=9600.

Step-by-step Validation

Once the code is uploaded and the serial monitor is running, perform the following validation checkpoints to ensure the logic and hardware are functioning correctly.

  • Checkpoint 1: Initialization and Idle State

    • Action: Observe the serial monitor immediately after uploading, with the room temperature below 25°C.
    • Expected Observation: The serial monitor prints the initialization banner. The servo moves to the 0-degree position. Logs indicate a temperature below 25.0°C, Override: STANDBY, and Vent State: CLOSED.
    • Pass Condition: Servo is physically at the zero position and logs reflect the closed state accurately.
  • Checkpoint 2: Heating Up (Crossing Upper Threshold)

    • Action: Gently pinch the LM35 sensor between your fingers to raise its temperature. Watch the serial monitor.
    • Expected Observation: The logged temperature will steadily rise. Once it hits 28.0°C or higher, the Vent State changes to OPEN.
    • Pass Condition: The SG90 servo physically rotates 90 degrees immediately when the log shows a temperature $\ge$ 28.0°C.
  • Checkpoint 3: Hysteresis Verification (Cooling Down)

    • Action: Let go of the LM35 and allow it to cool. Observe the logs as the temperature falls between 27.9°C and 25.1°C.
    • Expected Observation: The temperature drops, but the Vent State remains OPEN. The servo does not move.
    • Pass Condition: The system maintains its current state while in the hysteresis deadband, preventing rapid mechanical oscillation.
  • Checkpoint 4: Crossing Lower Threshold

    • Action: Continue letting the sensor cool (or blow gently on it) until the temperature drops to 25.0°C or below.
    • Expected Observation: The log updates the Vent State to CLOSED.
    • Pass Condition: The servo physically rotates back to the 0-degree position.
  • Checkpoint 5: Manual Override Activation

    • Action: While the temperature is below 25°C (vent normally closed), press and hold the limit switch.
    • Expected Observation: The next serial log (within 1 second) shows Override: ACTIVE and Vent State: OPEN.
    • Pass Condition: The servo immediately moves to 90 degrees despite the cold temperature. Releasing the switch should return the servo to 0 degrees on the next cycle.

Troubleshooting

If your prototype is not behaving as expected, consult the table below for common issues and their solutions.

Symptom Likely Cause Fix
LM35 reads ~0°C or ~500°C constantly Sensor wired backwards or floating ground. Disconnect power immediately. Check the flat face of the LM35. Left is 5V, Right is GND, Center is A0. Re-seat the jumper wires.
Temperature readings fluctuate wildly (±5°C)

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 automated greenhouse vent controller?




Question 2: Which component is used as the actuator to physically move the ventilation flap?




Question 3: Why is hysteresis logic incorporated into the system?




Question 4: How does the hysteresis implementation stabilize the system?




Question 5: What component provides the hardware-based manual bypass for emergencies or maintenance?




Question 6: What is the expected response latency when temperature thresholds are crossed?




Question 7: What is the expected range of motion for the servo driving the ventilation flap?




Question 8: What is an example of the distinct temperature thresholds used for hysteresis in this system?




Question 9: What type of control system is the automated greenhouse vent controller described as?




Question 10: What is the main benefit of agricultural automation in this context?




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: RS485 Relay Node with Arduino UNO

Practical case: RS485 Relay Node with Arduino UNO — hero

Objective and use case

What you’ll build: A robust, addressable remote relay node using an Arduino UNO that communicates over a long-distance RS485 bus to toggle a 5V relay based on targeted serial commands.

Why it matters / Use cases

  • Agricultural Automation: Control irrigation valves or greenhouse ventilation fans hundreds of meters away where standard 3.3V UART, I2C, or SPI signals would degrade.
  • Industrial Alert Systems: Trigger visual beacons across noisy factory floors. RS485 differential signaling provides high immunity to electromagnetic interference (EMI) from heavy machinery.
  • Wired Home Automation: Create a deterministic, hardwired network of distributed switches that guarantees sub-10ms response times without relying on Wi-Fi coverage.
  • Scalable Multi-drop Networks: Connect up to 32 addressable nodes on a single twisted pair of wires, ensuring each node only reacts to commands matching its specific ID.

Expected outcome

  • The Arduino successfully interprets addressed serial commands and toggles the 5V relay with near-zero latency.
  • Error-free communication is maintained over long wire runs (up to 1,200 meters at 9600 baud).
  • The node correctly filters bus traffic, ignoring commands intended for other devices on the shared network.

Audience: Embedded developers and automation engineers; Level: Intermediate

Architecture/flow: Master Controller → RS485 Bus (Twisted Pair) → MAX485 Transceiver → Arduino UNO (UART) → GPIO → 5V Relay Module

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 a low-voltage educational prototype, not a certified product. Before powering the setup, verify the wiring of your Arduino UNO R3, avoid shorting 5 V, GND or digital pins, disconnect power before changing connections, and use proper interface modules for relays, motors or external loads.

Conceptual block diagram

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

Functional architecture

Master Controller

RS485 Bus (Twisted Pair)

MAX485 Transceiver

Arduino UNO (UART)

GPIO

5V Relay Module

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

  • Familiarity with basic terminal operations and command-line interfaces.
  • Understanding of standard UART (Serial) communication (Baud rate, TX/RX).
  • Arduino CLI installed and added to your system’s PATH.
  • Python 3.11 installed (for running the master validation script).
  • The pyserial library installed in your Python environment (pip install pyserial).

Materials

  • Microcontroller: Arduino UNO R3 (ATmega328P).
  • Transceiver: MAX485 RS485 module (standard 8-pin breakout board with DI, DE, RE, RO pins).
  • Actuator: 5 V relay module (opto-isolated, active-high or active-low).
  • Master Interface: USB-to-RS485 adapter (to connect your PC to the RS485 bus for testing).
  • Passive Components: 1x 120-ohm resistor (for bus termination, recommended for long cable runs).
  • Cabling: Jumper wires, breadboard, and a twisted pair cable for the A/B RS485 lines.

Setup/Connection

RS485 is a half-duplex standard, meaning data travels in both directions, but only one direction at a time. The MAX485 chip uses Driver Enable (DE) and Receiver Enable (RE) pins to switch between transmitting and receiving. We will tie these two pins together and control them with a single digital pin on the Arduino.

We will use SoftwareSerial on pins 10 and 11 to communicate with the MAX485. This preserves the Arduino’s hardware serial port (pins 0 and 1) for USB debugging and uploading code without needing to disconnect the wiring.

Arduino to MAX485 Module Wiring

MAX485 Pin Arduino UNO Pin Function / Description
VCC 5V Power supply for the transceiver.
GND GND Common ground reference.
RO (Receiver Out) Digital Pin 10 Connects to Arduino Software RX.
RE (Receiver Enable) Digital Pin 2 Active LOW. Tied to DE.
DE (Driver Enable) Digital Pin 2 Active HIGH. Tied to RE.
DI (Driver In) Digital Pin 11 Connects to Arduino Software TX.
A USB-RS485 ‘A’ Non-inverting RS485 bus line.
B USB-RS485 ‘B’ Inverting RS485 bus line.

Note: Ensure the ground of the USB-to-RS485 adapter is connected to the Arduino’s ground to maintain a common reference voltage, especially for short-distance bench testing.

Arduino to 5V Relay Module Wiring

Relay Module Pin Arduino UNO Pin Function / Description
VCC / DC+ 5V Power supply for the relay coil and optocoupler.
GND / DC- GND Common ground reference.
IN / Signal Digital Pin 7 Control signal to trigger the relay.

Validated Code

The following sections contain the complete source code for both the Arduino node and the Python master script used to validate the bus.

Arduino Node Firmware

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

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

/*
 * rs485_relay_node.ino
 * 
 * Implements a half-duplex RS485 slave node.
 * Listens for "<NODE_ID>:<COMMAND>\n".
 * Valid commands: "ON", "OFF".
 */

#include <SoftwareSerial.h>

// Pin Definitions
#define RE_DE_PIN 2     // HIGH = Transmit, LOW = Receive
#define RELAY_PIN 7     // Relay control pin
#define RX_PIN 10       // SoftwareSerial RX
#define TX_PIN 11       // SoftwareSerial TX

// Node Configuration
const String NODE_ID = "N1";
const long BAUD_RATE = 9600;

// Initialize SoftwareSerial for RS485 communication
SoftwareSerial rs485(RX_PIN, TX_PIN);

void setup() {
  // Configure RS485 control pins
  pinMode(RE_DE_PIN, OUTPUT);
  digitalWrite(RE_DE_PIN, LOW); // Default to listen mode

  // Configure Relay pin
  pinMode(RELAY_PIN, OUTPUT);
  digitalWrite(RELAY_PIN, LOW); // Default relay state (adjust if active-low)

  // Initialize Serial ports
  Serial.begin(BAUD_RATE); // Hardware serial for debugging
  rs485.begin(BAUD_RATE);  // Software serial for RS485 bus

  Serial.println("System Boot: RS485 Relay Node");
  Serial.println("Node ID: " + NODE_ID);
  Serial.println("Listening for commands...");
}

void loop() {
  // Check if data is available on the RS485 bus
  if (rs485.available()) {
    // Read the incoming frame until a newline character
    String incomingMsg = rs485.readStringUntil('\n');
    incomingMsg.trim(); // Remove carriage returns or trailing spaces

    if (incomingMsg.length() > 0) {
      Serial.println("Bus traffic detected: " + incomingMsg);
      processCommand(incomingMsg);
    }
// ...

/*
 * rs485_relay_node.ino
 * 
 * Implements a half-duplex RS485 slave node.
 * Listens for "<NODE_ID>:<COMMAND>\n".
 * Valid commands: "ON", "OFF".
 */

#include <SoftwareSerial.h>

// Pin Definitions
#define RE_DE_PIN 2     // HIGH = Transmit, LOW = Receive
#define RELAY_PIN 7     // Relay control pin
#define RX_PIN 10       // SoftwareSerial RX
#define TX_PIN 11       // SoftwareSerial TX

// Node Configuration
const String NODE_ID = "N1";
const long BAUD_RATE = 9600;

// Initialize SoftwareSerial for RS485 communication
SoftwareSerial rs485(RX_PIN, TX_PIN);

void setup() {
  // Configure RS485 control pins
  pinMode(RE_DE_PIN, OUTPUT);
  digitalWrite(RE_DE_PIN, LOW); // Default to listen mode

  // Configure Relay pin
  pinMode(RELAY_PIN, OUTPUT);
  digitalWrite(RELAY_PIN, LOW); // Default relay state (adjust if active-low)

  // Initialize Serial ports
  Serial.begin(BAUD_RATE); // Hardware serial for debugging
  rs485.begin(BAUD_RATE);  // Software serial for RS485 bus

  Serial.println("System Boot: RS485 Relay Node");
  Serial.println("Node ID: " + NODE_ID);
  Serial.println("Listening for commands...");
}

void loop() {
  // Check if data is available on the RS485 bus
  if (rs485.available()) {
    // Read the incoming frame until a newline character
    String incomingMsg = rs485.readStringUntil('\n');
    incomingMsg.trim(); // Remove carriage returns or trailing spaces

    if (incomingMsg.length() > 0) {
      Serial.println("Bus traffic detected: " + incomingMsg);
      processCommand(incomingMsg);
    }
  }
}

void processCommand(String msg) {
  // Check if the message is addressed to this specific node
  if (msg.startsWith(NODE_ID + ":")) {
    // Extract the command portion after the colon
    int separatorIndex = msg.indexOf(':');
    String action = msg.substring(separatorIndex + 1);

    if (action == "ON") {
      digitalWrite(RELAY_PIN, HIGH);
      Serial.println("Action: Relay turned ON");
      sendAcknowledgment("ACK_ON");
    } 
    else if (action == "OFF") {
      digitalWrite(RELAY_PIN, LOW);
      Serial.println("Action: Relay turned OFF");
      sendAcknowledgment("ACK_OFF");
    }
    else {
      Serial.println("Warning: Unknown command received.");
    }
  } else {
    // Message is for another node or malformed; ignore quietly
    Serial.println("Ignored: Addressed to another node.");
  }
}

void sendAcknowledgment(String ackMsg) {
  // 1. Switch MAX485 to Transmit mode
  digitalWrite(RE_DE_PIN, HIGH);

  // 2. Allow a brief moment for the transceiver to stabilize
  delay(5); 

  // 3. Transmit the acknowledgment frame
  String fullAck = NODE_ID + ":" + ackMsg;
  rs485.println(fullAck);

  // 4. Wait for the serial transmission buffer to empty completely
  // This is critical. If we pull RE_DE low too early, the transmission is cut off.
  rs485.flush(); 

  // 5. Allow the final bit to hit the wire before switching states
  delay(5);

  // 6. Switch MAX485 back to Receive mode
  digitalWrite(RE_DE_PIN, LOW);

  Serial.println("Transmitted: " + fullAck);
}

Python Master Test Script

Save the following code as master_controller.py on your PC. This script acts as the central controller, generating RS485 traffic to test the Arduino node.

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

#!/usr/bin/env python3
"""
master_controller.py
Validates the Arduino RS485 Relay Node by sending specific string commands
over a USB-to-RS485 adapter and waiting for acknowledgments.
"""

import serial
import time
import sys

# Windows: 'COM3', Linux: '/dev/ttyUSB0', macOS: '/dev/cu.usbserial-...'
PORT = '/dev/ttyUSB0' 
BAUD = 9600
TIMEOUT = 2.0

def send_and_wait(ser, node_id, command):
    """Sends a formatted command and waits for an acknowledgment."""
    frame = f"{node_id}:{command}\n"
    print(f"\n[Master] Sending : {frame.strip()}")

    # Send the byte-encoded string over the serial port
    ser.write(frame.encode('ascii'))

    # Wait for the node to process and respond
    time.sleep(0.1)

    # Read the response if available
    response_received = False
    start_time = time.time()

    while (time.time() - start_time) < TIMEOUT:
        if ser.in_waiting > 0:
            response = ser.readline().decode('ascii', errors='ignore').strip()
            print(f"[Master] Received: {response}")
            response_received = True
            break
        time.sleep(0.05)

    if not response_received:
        print("[Master] Error: No response received (Timeout).")
# ...

#!/usr/bin/env python3
"""
master_controller.py
Validates the Arduino RS485 Relay Node by sending specific string commands
over a USB-to-RS485 adapter and waiting for acknowledgments.
"""

import serial
import time
import sys

# Windows: 'COM3', Linux: '/dev/ttyUSB0', macOS: '/dev/cu.usbserial-...'
PORT = '/dev/ttyUSB0' 
BAUD = 9600
TIMEOUT = 2.0

def send_and_wait(ser, node_id, command):
    """Sends a formatted command and waits for an acknowledgment."""
    frame = f"{node_id}:{command}\n"
    print(f"\n[Master] Sending : {frame.strip()}")

    # Send the byte-encoded string over the serial port
    ser.write(frame.encode('ascii'))

    # Wait for the node to process and respond
    time.sleep(0.1)

    # Read the response if available
    response_received = False
    start_time = time.time()

    while (time.time() - start_time) < TIMEOUT:
        if ser.in_waiting > 0:
            response = ser.readline().decode('ascii', errors='ignore').strip()
            print(f"[Master] Received: {response}")
            response_received = True
            break
        time.sleep(0.05)

    if not response_received:
        print("[Master] Error: No response received (Timeout).")

def main():
    try:
        print(f"Opening port {PORT} at {BAUD} baud...")
        with serial.Serial(PORT, BAUD, timeout=TIMEOUT) as ser:
            time.sleep(2) # Allow port initialization

            print("--- Starting Validation Sequence ---")

            # Test 1: Turn Relay ON
            send_and_wait(ser, "N1", "ON")
            time.sleep(2)

            # Test 2: Turn Relay OFF
            send_and_wait(ser, "N1", "OFF")
            time.sleep(2)

            # Test 3: Test Address Filtering (Should be ignored by N1)
            send_and_wait(ser, "N2", "ON")

            print("\n--- Validation Sequence Complete ---")

    except serial.SerialException as e:
        print(f"Serial Port Error: {e}")
        sys.exit(1)
    except KeyboardInterrupt:
        print("\nProcess aborted by user.")
        sys.exit(0)

if __name__ == '__main__':
    main()

Build/Flash/Run commands

Use the Arduino CLI to compile and flash the firmware to your Arduino UNO R3. Ensure your terminal is open in the directory containing rs485_relay_node.

Task 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 rs485_relay_node
Upload to board arduino-cli upload --fqbn arduino:avr:uno --port <PORT> rs485_relay_node

Workflow steps:
1. Connect the Arduino UNO to your PC via USB.
2. Identify the port (e.g., COM3 on Windows or /dev/ttyACM0 on Linux) using arduino-cli board list.
3. Run the compilation command to ensure there are no syntax errors.
4. Run the upload command, replacing <PORT> with your specific port identifier.
5. Open a serial monitor (`arduino-

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 main objective of the project described in the text?




Question 2: Which communication protocol is used for the long-distance bus in this project?




Question 3: What component does the Arduino UNO toggle based on targeted serial commands?




Question 4: Why is RS485 preferred over standard UART, I2C, or SPI for agricultural automation?




Question 5: What advantage does RS485 offer in noisy industrial environments?




Question 6: What is a key benefit of using this system for wired home automation?




Question 7: How many addressable nodes can be connected on a single twisted pair of wires in this scalable network?




Question 8: How does a specific node know when to react to a command on the multi-drop network?




Question 9: What is the expected latency when the Arduino toggles the relay?




Question 10: Up to what distance can error-free communication be maintained using this setup?




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: fridge door alarm with Arduino UNO

Practical case: fridge door alarm with Arduino UNO — hero

Objective and use case

What you’ll build: A prototype environmental monitor and alarm system that tracks refrigerator door status and internal temperature to prevent food spoilage and energy waste.

Why it matters / Use cases

  • Commercial & Lab Storage: Prevents walk-in freezers from being left ajar and ensures temperature-sensitive chemical samples remain within strictly defined thermal thresholds.
  • Domestic Energy Conservation: Eliminates energy waste and prevents compressor burnout caused by prolonged open doors in residential settings.
  • Hardware State-Machine Education: Demonstrates practical non-blocking microcontroller code using millis() to handle multiple independent sensors simultaneously without thread blocking.

Expected outcome

  • The system continuously polls the LM35 temperature sensor every 2000ms (0.5 Hz) and streams data to the Serial Monitor for logging.
  • A magnetic reed switch accurately detects open/closed door states with near-zero latency, triggering immediate state-machine transitions and temporal alarms.

Audience: Embedded Systems Developers, STEM Educators; Level: Intermediate

Architecture/flow: LM35 (Analog) & Reed Switch (Digital GPIO) → Microcontroller (Non-blocking State Machine) → Serial Monitor Logging & Alarm Triggers

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 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 a low-voltage educational prototype, not a certified product. Before powering the setup, verify the wiring of your Arduino UNO R3, avoid shorting 5 V, GND or digital pins, disconnect power before changing connections, and use proper interface modules for relays, motors or external loads.

Conceptual block diagram

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

Functional architecture

LM35 (Analog) & Reed Switch (Digital GPIO)

Microcontroller (Non-blocking State Machine)

Serial Monitor Logging & Alarm Triggers

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.

Validation Method and Accuracy

To validate the accuracy of the LM35 sensor readings, place a calibrated commercial digital thermometer in the exact same location as the LM35 inside the refrigerator.
* Expected Evidence: The serial output of the Arduino should report temperatures within ±0.5°C of the calibrated thermometer at standard refrigerator temperatures (2.0°C to 8.0°C) after allowing 10 minutes for the sensor casing to reach thermal equilibrium.

Prerequisites

Before beginning this tutorial, ensure you have the following tools and knowledge ready:
* A host computer running Linux, macOS, or Windows.
* Arduino CLI installed and added to your system’s PATH.
* A USB Type-A to Type-B cable to connect the Arduino UNO to your computer.
* Basic understanding of how to use a breadboard and jumper wires.

Materials

You will need the exact components listed below to build this prototype.

Component Exact Model / Specification Quantity Role in Prototype
Microcontroller Arduino UNO R3 (ATmega328P) 1 The central processing unit evaluating sensor logic.
Temperature Sensor LM35 (LM35DZ TO-92 package) 1 Precision analog sensor providing 10mV/°C linear output.
Magnetic Sensor Standard Reed Switch (Normally Open) 1 Detects the presence of a magnet to determine door state.
Magnet Small Neodymium or Ceramic Magnet 1 Mounted to the door to actuate the reed switch.
Audio Actuator Piezo Buzzer (Passive or Active) 1 Emits audible alarm tones.
Wiring Male-to-Male Jumper Wires ~10 Connects components on the breadboard to the Arduino.
Breadboard Standard Half-Size Breadboard 1 Provides a solderless prototyping foundation.

Note: We do not require an external pull-up resistor for the reed switch because we will utilize the ATmega328P’s internal pull-up resistor via software.

Setup/Connection

Follow these instructions carefully. Ensure your Arduino is disconnected from USB power while wiring.

1. LM35 Temperature Sensor Wiring

Hold the LM35 so the flat face with the printed text is facing you, and the three pins are pointing down.
* Left Pin (VCC): Connect to the 5V pin on the Arduino.
* Middle Pin (OUT): Connect to the A0 (Analog 0) pin on the Arduino.
* Right Pin (GND): Connect to a GND pin on the Arduino.

2. Reed Switch Wiring

A reed switch has two terminals and is non-polarized (orientation does not matter).
* Terminal 1: Connect to Digital Pin 2 on the Arduino.
* Terminal 2: Connect to a GND pin on the Arduino.
* Logic Note: We will configure Pin 2 as INPUT_PULLUP. When the magnet is near (door closed), the switch closes, connecting Pin 2 to GND (reading LOW). When the door opens, the switch opens, and the internal resistor pulls Pin 2 to 5V (reading HIGH).

3. Piezo Buzzer Wiring

If your piezo buzzer has a longer leg (positive) and a shorter leg (negative), follow this polarization.
* Positive Terminal (Longer leg / Red wire): Connect to Digital Pin 8 on the Arduino.
* Negative Terminal (Shorter leg / Black wire): Connect to a GND pin on the Arduino.

Arduino Sketch

Create a new directory named FridgeAlarm and create the following file inside it named FridgeAlarm.ino.

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

/*
 * Fridge Door and Temperature Alarm
 * Architecture: Arduino UNO R3 (ATmega328P)
 * Sensors: Reed Switch (Pin 2), LM35 (Pin A0)
 * Actuator: Piezo Buzzer (Pin 8)
 */

// Hardware Pin Definitions
const int REED_PIN = 2;
const int BUZZER_PIN = 8;
const int LM35_PIN = A0;

// Configuration Thresholds
const float TEMP_THRESHOLD_C = 8.0;          // Alarm triggers if temperature exceeds 8.0 Celsius
const unsigned long DOOR_TIMEOUT_MS = 20000; // Alarm triggers if door is open for 20,000 ms (20 seconds)
const unsigned long TEMP_READ_INTERVAL = 2000; // Read temperature every 2,000 ms (2 seconds)

// State Tracking Variables
unsigned long doorOpenedAt = 0;
unsigned long lastTempRead = 0;

bool isDoorOpen = false;
bool tempAlarmActive = false;
bool doorAlarmActive = false;
float currentTemp = 0.0;

void setup() {
    // Initialize serial communication for data logging
    Serial.begin(115200);

    // Configure pins
    // INPUT_PULLUP applies an internal 20k-50k ohm resistor to 5V.
    pinMode(REED_PIN, INPUT_PULLUP);
    pinMode(BUZZER_PIN, OUTPUT);

    Serial.println("--- Fridge Monitor Initialized ---");
    Serial.print("Temp Threshold: ");
    Serial.print(TEMP_THRESHOLD_C);
    Serial.println(" C");
    Serial.print("Door Timeout: ");
    Serial.print(DOOR_TIMEOUT_MS / 1000);
    Serial.println(" seconds");
}

void loop() {
    // Capture current time once per loop iteration
    unsigned long currentMillis = millis();

    // ---------------------------------------------------------
    // 1. Evaluate Door State
    // ---------------------------------------------------------
    // Switch is closed (LOW) when magnet is present (door closed).
    // Switch is open (HIGH) when magnet is removed (door open).
    bool currentDoorState = (digitalRead(REED_PIN) == HIGH);

    // Detect state change: Door just opened
    if (currentDoorState && !isDoorOpen) {
        isDoorOpen = true;
        doorOpenedAt = currentMillis;
        Serial.println("[EVENT] Door Opened.");
    } 
    // Detect state change: Door just closed
    else if (!currentDoorState && isDoorOpen) {
        isDoorOpen = false;
        doorAlarmActive = false;
        Serial.println("[EVENT] Door Closed.");
        noTone(BUZZER_PIN); // Silence any active door alarms
    }
// ...

/*
 * Fridge Door and Temperature Alarm
 * Architecture: Arduino UNO R3 (ATmega328P)
 * Sensors: Reed Switch (Pin 2), LM35 (Pin A0)
 * Actuator: Piezo Buzzer (Pin 8)
 */

// Hardware Pin Definitions
const int REED_PIN = 2;
const int BUZZER_PIN = 8;
const int LM35_PIN = A0;

// Configuration Thresholds
const float TEMP_THRESHOLD_C = 8.0;          // Alarm triggers if temperature exceeds 8.0 Celsius
const unsigned long DOOR_TIMEOUT_MS = 20000; // Alarm triggers if door is open for 20,000 ms (20 seconds)
const unsigned long TEMP_READ_INTERVAL = 2000; // Read temperature every 2,000 ms (2 seconds)

// State Tracking Variables
unsigned long doorOpenedAt = 0;
unsigned long lastTempRead = 0;

bool isDoorOpen = false;
bool tempAlarmActive = false;
bool doorAlarmActive = false;
float currentTemp = 0.0;

void setup() {
    // Initialize serial communication for data logging
    Serial.begin(115200);

    // Configure pins
    // INPUT_PULLUP applies an internal 20k-50k ohm resistor to 5V.
    pinMode(REED_PIN, INPUT_PULLUP);
    pinMode(BUZZER_PIN, OUTPUT);

    Serial.println("--- Fridge Monitor Initialized ---");
    Serial.print("Temp Threshold: ");
    Serial.print(TEMP_THRESHOLD_C);
    Serial.println(" C");
    Serial.print("Door Timeout: ");
    Serial.print(DOOR_TIMEOUT_MS / 1000);
    Serial.println(" seconds");
}

void loop() {
    // Capture current time once per loop iteration
    unsigned long currentMillis = millis();

    // ---------------------------------------------------------
    // 1. Evaluate Door State
    // ---------------------------------------------------------
    // Switch is closed (LOW) when magnet is present (door closed).
    // Switch is open (HIGH) when magnet is removed (door open).
    bool currentDoorState = (digitalRead(REED_PIN) == HIGH);

    // Detect state change: Door just opened
    if (currentDoorState && !isDoorOpen) {
        isDoorOpen = true;
        doorOpenedAt = currentMillis;
        Serial.println("[EVENT] Door Opened.");
    } 
    // Detect state change: Door just closed
    else if (!currentDoorState && isDoorOpen) {
        isDoorOpen = false;
        doorAlarmActive = false;
        Serial.println("[EVENT] Door Closed.");
        noTone(BUZZER_PIN); // Silence any active door alarms
    }

    // Check if door has been open longer than the timeout
    if (isDoorOpen && (currentMillis - doorOpenedAt >= DOOR_TIMEOUT_MS)) {
        if (!doorAlarmActive) {
            Serial.println("[ALARM] Door Open Timeout Exceeded!");
            doorAlarmActive = true;
        }
    }

    // ---------------------------------------------------------
    // 2. Evaluate Temperature
    // ---------------------------------------------------------
    if (currentMillis - lastTempRead >= TEMP_READ_INTERVAL) {
        lastTempRead = currentMillis;

        // Read 10-bit ADC value (0-1023)
        int adcValue = analogRead(LM35_PIN);

        // Convert ADC value to voltage (5V reference)
        // Note: 1024.0 is the correct divisor for a 10-bit AVR ADC
        float voltage = (adcValue / 1024.0) * 5.0;

        // LM35 outputs 10mV (0.01V) per degree Celsius
        currentTemp = voltage / 0.01;

        Serial.print("[DATA] Current Temp: ");
        Serial.print(currentTemp);
        Serial.println(" C");

        // Trigger temperature alarm if threshold is exceeded
        if (currentTemp > TEMP_THRESHOLD_C) {
            if (!tempAlarmActive) {
                Serial.println("[ALARM] Temperature Critical!");
                tempAlarmActive = true;
            }
        } else {
            if (tempAlarmActive) {
                Serial.println("[RESOLVED] Temperature returned to normal.");
                tempAlarmActive = false;
                noTone(BUZZER_PIN); // Silence temp alarm
            }
        }
    }

    // ---------------------------------------------------------
    // 3. Actuate Alarms (Non-blocking Buzzer Logic)
    // ---------------------------------------------------------
    // Priority 1: Door Alarm (Fast high-pitched beep)
    if (doorAlarmActive) {
        // Toggle tone every 250ms
        if ((currentMillis / 250) % 2 == 0) {
            tone(BUZZER_PIN, 1200); // 1200 Hz
        } else {
            noTone(BUZZER_PIN);
        }
    } 
    // Priority 2: Temperature Alarm (Slow lower-pitched beep)
    // Only sound if the door alarm isn't already sounding
    else if (tempAlarmActive && !doorAlarmActive) {
        // Toggle tone every 1000ms
        if ((currentMillis / 1000) % 2 == 0) {
            tone(BUZZER_PIN, 600); // 600 Hz
        } else {
            noTone(BUZZER_PIN);
        }
    }
    // No alarms active
    else if (!doorAlarmActive && !tempAlarmActive) {
        noTone(BUZZER_PIN);
    }
}

Compilation and Upload Commands

Navigate to the directory just above your FridgeAlarm folder in your terminal. Ensure your Arduino UNO R3 is connected to your computer via USB. Identify your serial port (e.g., COM3 on Windows, /dev/ttyACM0 on Linux, or /dev/cu.usbmodem14101 on macOS) and run the following exact commands to prepare the environment, compile, and upload the sketch.

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

# Install the AVR core for Arduino UNO
arduino-cli core install arduino:avr

# Compile the sketch for the Arduino UNO R3
arduino-cli compile --fqbn arduino:avr:uno FridgeAlarm

# Upload the compiled code to the board (replace <PORT> with your actual port)
arduino-cli upload --fqbn arduino:avr:uno --port <PORT> FridgeAlarm

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 purpose of the prototype system described in the article?




Question 2: Which of the following is a commercial or lab storage use case for this system?




Question 3: How does the system help with domestic energy conservation?




Question 4: What specific function is used to demonstrate practical non-blocking microcontroller code?




Question 5: How often does the system poll the LM35 temperature sensor?




Question 6: What component is used to accurately detect the open/closed door states?




Question 7: Where does the system stream the LM35 temperature sensor data for logging?




Question 8: Who is the intended audience for this project?




Question 9: What is the specified difficulty level of this project?




Question 10: What triggers immediate state-machine transitions and temporal alarms in the system?




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

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

Follow me: