Desk alarm: acknowledge and silence an event

Build a PIR alarm with separate monitoring and acknowledgment commands. Silence the sound while retaining the LED indication, require a quiet input before arming and define button priority.

Integrated projects and user controlsIntermediate105 min

What you will learn

  • Distinguish disarming from acknowledging a latched event.
  • Require continuous PIR LOW for 2 s before monitoring.
  • Process two debounced buttons with ARM/DISARM priority.
  • Drive an active buzzer through an additional PN2222A stage.
  • Coordinate seven alarm states, LED patterns and a RAM event counter.

Before you start

Courses 15 and 03: a PIR alarm state machine, INPUT_PULLUP button debouncing and non-blocking elapsed-time control.

Reference setup

Board in Arduino IDE
ESP32 Dev Module
Arduino-ESP32
3.3.12
Serial Monitor
115200 baud

This is a reference profile, not an identification of the pictured board. Adapt the GPIO mapping and verify the circuit before using ESP32-C3/S2/S3 or a differently labelled board.

What we will build

We will build a desk alarm model with two buttons: one arms or disarms monitoring, while the other acknowledges that we noticed an event. A PIR sensor detects motion near the desk. When it triggers an alarm, a red LED stays on and an active buzzer produces short repeating signals. Pressing the acknowledgment button silences the buzzer, while the LED remains lit as a reminder of the latched event.

Compared with course 15, this lesson distinguishes silencing, disarming and being ready for a new event. Before monitoring begins, the program requires PIR to remain LOW continuously for two seconds. This is an event-handling demonstration near a desk. It does not establish whether an object was moved or stolen, and it is not a security device.

What you will learn

  • Organize an alarm with separate ARM/DISARM and ACK commands.
  • Distinguish acknowledgment of an event from clearing it.
  • Require an uninterrupted quiet input before monitoring begins.
  • Assign priority to commands accepted at the same time.
  • Process two buttons without repeating an action while either is held.
  • Coordinate program state, LED indication and audible output patterns.

Prerequisites

Complete course 15 on the PIR alarm and course 03 on buttons. You should understand INPUT_PULLUP, contact bounce and elapsed-time calculations with millis(). Review the transistor output stage from course 13 if you have not connected the active buzzer independently. Before testing the combined circuit, check both button inputs: each should read HIGH when released and LOW when pressed.

Required equipment

ComponentQuantitySpecification and notes
Development board with a classic ESP32-WROOM-32 module1Match the reference GPIO labels; check physical header positions on the actual board.
USB data and power cable1Use the connector fitted to your board; the pictured kit lists Micro-USB.
Solderless breadboard1The kit lists 830 tie points. Check whether the power rails are split.
HC-SR501 PIR motion sensor1Module supplied from the board USB 5 V rail; OUT is 3.3 V. Verify pin order, H/L jumper and output pulse duration.
Active buzzer — 5 V, up to 20 mA reference1The photo lists an active buzzer but does not establish its voltage or current. For this circuit use a documented 5 V model with operating current no higher than 20 mA.
Additional PN2222A NPN transistor1Required extra — not in kit photoADDITIONAL, not listed in the kit. Identify B/C/E for the exact manufacturer; PN2222A and P2N2222A may have different lead orders.

ADDITIONAL: the transistor is not listed in the kit photograph.

Additional 1N4148 protective diode1Required extra — not in kit photoADDITIONAL, not listed in the kit. The band marks cathode K. In the buzzer circuit the cathode faces +5 V.

ADDITIONAL: the diode is not listed in the kit photograph.

1 kΩ resistor1Base resistor; course 14 also uses one across the piezo. The kit quantity at this resistance is unconfirmed.
10 kΩ resistor1R2 is the base pull-down from Q1.B to GND.
Momentary pushbutton2Use two terminals connected only when pressed; two legs on the same side may already be joined.
Red LED1Anode A and cathode K; identify polarity on the actual part.
220 Ω resistor1One current-limiting resistor per LED branch; use the kit’s 220 Ω resistors.
Jumper wires18Use male-to-male or female-to-male leads to suit the board headers.

Approximate quantity; depends on layout and connector types.

The PN2222A transistor and 1N4148 diode are additional parts not listed in the kit photograph. One of each is required for this circuit. The reference active buzzer uses 5 V and has a rated operating current no greater than 20 mA. Check its actual marking, type and current rating; appearance alone does not confirm these properties.

Use a classic ESP32-WROOM-32, an HC-SR501 supplied at 5 V with a 3.3 V logic output, two momentary buttons and a red LED. The 1 kΩ, 10 kΩ and 220 Ω resistors serve different purposes. Verify values and available quantities before wiring. The complete example receives power through USB.

States, acknowledgment and command priority

StateMeaning and outputsTransition
WARMUPSettling for 60 s; LED and buzzer off.Automatically enters DISARMED.
DISARMEDMonitoring off; both outputs off.A fresh ARM press starts the exit delay.
EXIT_DELAYTen seconds to move away; LED on for 250 ms, off for 250 ms.After 10 s, enters WAIT_CLEAR.
WAIT_CLEARWaits for quiet PIR; the same fast LED pattern.Continuous PIR LOW for 2 s enters ARMED.
ARMEDMonitoring active; LED on for 50 ms, off for 1950 ms.PIR HIGH triggers the alarm.
ALARMLED steady; buzzer on for 200 ms, off for 800 ms.ACK silences; ARM disarms.
ACKNOWLEDGEDEvent acknowledged; LED steady, buzzer off.ARM disarms and clears the active event state.

ARM/DISARM takes priority over ACK when both presses are accepted in the same loop iteration. From every state except WARMUP and DISARMED, it returns the system to DISARMED. ACK acts only in ALARM and is ignored elsewhere. Acknowledgment does not mean that the cause has disappeared, and it does not automatically begin another monitoring cycle.

Wiring diagram and connections

Wiring diagram

D1 is the buzzer protective diode; D2 is the red LED. Additional Q1 and D1 are not listed in the kit. SW1 arms/disarms the system; SW2 acknowledges an active alarm and silences it. PIR and buzzer use the verified USB5V rail; GPIO signals remain 3.3 V.

Wiring diagram — electrical connections listed in the table below
Wiring diagramEnlargeSVGPNG
Connections · Wiring diagram
FromToConnection
ESP32.GPIO26R1.1R1, 1 kΩ, limits base current.
R1.2Q1.BBase of the additional PN2222A; verify physical B/C/E lead order.
Q1.BR2.1R2, 10 kΩ, pulls the base down.
R2.2ESP32.GNDOther end of the base pull-down resistor.
Q1.EESP32.GNDEmitter to common ground.
ESP32.USB5VBZ1.PLUSDocumented 5 V active buzzer, up to 20 mA operating current.
BZ1.MINUSQ1.CBuzzer negative terminal to collector.
D1.ABZ1.MINUSProtective 1N4148 anode at the buzzer negative terminal.
D1.KESP32.USB5VDiode cathode, marked with a band, to +5 V.
ESP32.USB5VU1.VCCHC-SR501: verified 5 V rail derived from the board USB supply.
ESP32.GNDU1.GNDCommon ground.
U1.OUTESP32.GPIO33PIR OUT, 3.3 V logic, to GPIO33.
ESP32.GPIO25R3.1LED output (red) to its 220 Ω resistor.
R3.2D2.ASeries resistor to anode A.
D2.KESP32.GNDCathode K to ground.
ESP32.GPIO27SW1.CONTACT_AARM/DISARM button; INPUT_PULLUP input.
SW1.CONTACT_BESP32.GNDThe contact pair that connects only while pressed.
ESP32.GPIO32SW2.CONTACT_AACK button; INPUT_PULLUP input.
SW2.CONTACT_BESP32.GNDThe contact pair that connects only while pressed.
  1. Disconnect USB and verify your transistor's B, C and E terminals using its manufacturer's data sheet. PN2222A and the similarly named P2N2222A need not have the same lead order.
  2. Connect ESP32 GND, PIR GND and Q1 emitter to common ground. Also connect the return paths for the LED and both buttons.
  3. Connect GPIO26 → R1, 1 kΩ → Q1 base. Connect R2, 10 kΩ between Q1 base and GND; this is a pull-down, not a connection to 3V3.
  4. Connect Q1 collector to BZ1 negative, and the buzzer positive terminal to the verified USB 5 V rail.
  5. Connect D1 across BZ1: anode to buzzer negative, banded cathode to positive 5 V.
  6. Supply PIR U1 from the same USB 5 V rail. Connect OUT to GPIO33.
  7. Connect ARM/DISARM button SW1 between GPIO27 and GND, and ACK button SW2 between GPIO32 and GND. Select contacts that actually connect when pressed.
  8. Connect GPIO25 → R3, 220 Ω → red LED D2 anode, with its cathode to GND.

D1 is the protective diode, while D2 is the LED. When GPIO26 becomes HIGH, the transistor provides a current path through the buzzer to GND. Buzzer current does not pass through the GPIO. Check the board's ability to power all connected loads, and do not add an independent 5 V supply in parallel with USB. PIR OUT must remain compatible with ESP32 3.3 V logic.

Preparing the Arduino environment

In Arduino IDE 2.x, select esp32 by Espressif Systems 3.3.12, ESP32 Dev Module and your board's port. Open desk_alarm/desk_alarm.ino. No additional libraries are required; set Serial Monitor to 115200 baud. Label the buttons on the model so their functions remain clear without looking at the program.

Check GPIO33 for PIR, GPIO27 for ARM, GPIO32 for ACK, GPIO26 for the transistor output and GPIO25 for the LED. When reusing the circuit from course 15, GPIO32 is the new connection for the second button; the other listed connections retain their functions.

Complete Arduino sketch

desk_alarm.ino Arduino / C++
Download .ino

#include <Arduino.h>

constexpr uint8_t PIR_PIN = 33;
constexpr uint8_t BUZZER_PIN = 26;
constexpr uint8_t LED_PIN = 25;
constexpr uint8_t ARM_PIN = 27;
constexpr uint8_t ACK_PIN = 32;
constexpr uint32_t DEBOUNCE_MS = 30;
constexpr uint32_t WARMUP_MS = 60000;
constexpr uint32_t EXIT_DELAY_MS = 10000;
constexpr uint32_t CLEAR_MS = 2000;
// A fresh event needs 30 ms at one level. A held or inhibited press must
// release stably before another event can be accepted.
struct DebouncedButton {
  uint8_t pin;
  bool rawHigh = true, stableHigh = true, armed = true;
  bool pressed = false, released = false;
  uint32_t rawChangedAt = 0;
  explicit DebouncedButton(uint8_t inputPin) : pin(inputPin) {}
  void begin(uint32_t now) {
    pinMode(pin, INPUT_PULLUP);
    rawHigh = stableHigh = digitalRead(pin) == HIGH;
    armed = rawHigh;
    rawChangedAt = now;
    pressed = released = false;
  }
  void update(uint32_t now) {
    pressed = released = false;
    const bool readingHigh = digitalRead(pin) == HIGH;
    if (readingHigh != rawHigh) {
      rawHigh = readingHigh;
      rawChangedAt = now;
    }
    if (static_cast<uint32_t>(now - rawChangedAt) < DEBOUNCE_MS) return;
    if (rawHigh != stableHigh) {
      stableHigh = rawHigh;
      if (stableHigh) released = true;
      else if (armed) {
        pressed = true;
        armed = false;
      }
    }
    // Also recovers from an inhibited LOW pulse too short to become stable.
    if (rawHigh && stableHigh) armed = true;
  }
  void inhibit() { armed = false; pressed = false; }
  bool isReleased(uint32_t now) const {
    return rawHigh && stableHigh &&
           static_cast<uint32_t>(now - rawChangedAt) >= DEBOUNCE_MS;
  }
};

DebouncedButton armButton(ARM_PIN), ackButton(ACK_PIN);
enum AlarmState { WARMUP, DISARMED, EXIT_DELAY, WAIT_CLEAR, ARMED, ALARM, ACKNOWLEDGED };
AlarmState state = WARMUP;
uint32_t stateSince = 0;
bool clearTiming = false;
uint32_t clearSince = 0;
uint32_t eventCount = 0;
void enterState(AlarmState next, uint32_t now);

void enterState(AlarmState next, uint32_t now) {
  state = next;
  stateSince = now;
  clearTiming = false;
  Serial.print("State=");
  switch (state) {
    case WARMUP: Serial.println("WARMUP"); break;
    case DISARMED: Serial.println("DISARMED"); break;
    case EXIT_DELAY: Serial.println("EXIT_DELAY"); break;
    case WAIT_CLEAR: Serial.println("WAIT_CLEAR"); break;
    case ARMED: Serial.println("ARMED"); break;
    case ALARM: Serial.println("ALARM"); break;
    case ACKNOWLEDGED: Serial.println("ACKNOWLEDGED"); break;
  }
}

void updateOutputs(uint32_t now) {
  const uint32_t elapsed = static_cast<uint32_t>(now - stateSince);
  bool led = false, buzzer = false;
  if (state == EXIT_DELAY || state == WAIT_CLEAR) led = elapsed % 500UL < 250UL;
  else if (state == ARMED) led = elapsed % 2000UL < 50UL;
  else if (state == ALARM) {
    led = true;
    buzzer = elapsed % 1000UL < 200UL;
  } else if (state == ACKNOWLEDGED) led = true;
  digitalWrite(LED_PIN, led ? HIGH : LOW);
  digitalWrite(BUZZER_PIN, buzzer ? HIGH : LOW);
}

void setup() {
  Serial.begin(115200);
  pinMode(BUZZER_PIN, OUTPUT); digitalWrite(BUZZER_PIN, LOW);
  pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW);
  pinMode(PIR_PIN, INPUT);
  const uint32_t now = millis();
  armButton.begin(now); ackButton.begin(now);
  enterState(WARMUP, now);
}

void loop() {
  const uint32_t now = millis();
  armButton.update(now);
  ackButton.update(now);
  const bool pirHigh = digitalRead(PIR_PIN) == HIGH;
  if (state == WARMUP) {
    if (static_cast<uint32_t>(now - stateSince) >= WARMUP_MS) {
      // Suppress unfinished raw presses at this boundary as well as events.
      if (!armButton.isReleased(now)) armButton.inhibit();
      if (!ackButton.isReleased(now)) ackButton.inhibit();
      enterState(DISARMED, now);
    }
  } else if (armButton.pressed) {
    // ARM/DISARM has priority over ACK and sensor events in this iteration.
    enterState(state == DISARMED ? EXIT_DELAY : DISARMED, now);
  } else if (ackButton.pressed && state == ALARM) {
    enterState(ACKNOWLEDGED, now);  // Keep the event latched, silence the buzzer.
  } else {
    if (state == EXIT_DELAY && static_cast<uint32_t>(now - stateSince) >= EXIT_DELAY_MS) {
      enterState(WAIT_CLEAR, now);
    }
    if (state == WAIT_CLEAR) {
      if (pirHigh) clearTiming = false;
      else if (!clearTiming) {
        clearTiming = true;
        clearSince = now;
      } else if (static_cast<uint32_t>(now - clearSince) >= CLEAR_MS) {
        enterState(ARMED, now);
      }
    }
    if (state == ARMED && pirHigh) {
      ++eventCount;
      enterState(ALARM, now);
      Serial.print("Events since reset="); Serial.println(eventCount);
    }
  }
  updateOutputs(now);
}

Click Verify, then Upload. Keep all original intervals for the first experiment. Reset starts another PIR warm-up and returns the event counter to zero.

Understanding the sketch

AlarmState lists seven states. state stores the current one, and stateSince stores its entry time. enterState() changes the state, establishes a new time reference and clears any in-progress quiet-PIR measurement. updateOutputs() derives the LED and buzzer values from the state and elapsed time.

Each DebouncedButton tracks its own input. An accepted press requires a stable level for 30 ms and produces only one event. Holding a button during reset does not execute a command. Inputs continue to be read during WARMUP, but commands have no effect. At its end, the program also suppresses a press that began just before the boundary but has not yet qualified. A stable release followed by a new press is required.

In WAIT_CLEAR, clearTiming records whether a quiet interval is being measured, while clearSince records its start. Every observed HIGH cancels that interval. Only a new uninterrupted 2000 ms at LOW permits ARMED. An earlier quiet period during the exit delay does not count toward these two seconds.

The transition from ARMED to ALARM increments eventCount. Later acknowledgment, the PIR signal ending and a held button do not create additional events. This counter stays in RAM until reset; it is not a persistent event log. ACK retains the active event through ACKNOWLEDGED, whereas ARM switches both outputs off and returns to DISARMED. Subtracting uint32_t timestamps allows these short intervals to work across timer rollover.

Experiments and observations

Wait for the initial 60 seconds. Press and release ARM, move away during the ten-second delay, then wait until PIR has remained LOW for at least two seconds. Only the infrequent LED pulse indicates active monitoring. Create motion, wait for the alarm and press ACK. Next press ARM to disarm. To start another monitoring cycle, release ARM and press it again.

ExperimentExpected result
Hold ARM through the end of warm-up.The system stays DISARMED until release and a new press.
Keep PIR HIGH after the exit delay.It stays in WAIT_CLEAR, without sound.
In WAIT_CLEAR, produce 1.5 s LOW, a short HIGH, then LOW.The two-second interval restarts after the last HIGH.
Let PIR become LOW after the alarm triggers.The alarm stays latched until a user command.
Press ACK during an alarm.Sound stops and the LED remains steadily lit.
Create motion again after ACK.The system remains ACKNOWLEDGED; no new sound or counter increment occurs.
Have ARM and ACK qualify together during an alarm.ARM wins and the system becomes DISARMED.

The table lists expected results to check on the actual circuit. For an exact simultaneous-event check, what matters is when the sketch accepts the presses after debouncing, rather than two buttons merely appearing to be touched together.

Common problems

SymptomCheck and explanation
The LED keeps blinking rapidly long after ten seconds.PIR has not remained LOW continuously for two seconds; inspect motion and the module's hold setting.
ACK does not start monitoring.That is not its function; ARM starts a cycle from DISARMED.
The LED remains on after ACK.It deliberately preserves the acknowledged event indication; ARM clears it.
There is no sound although the LED indicates an alarm.Check BZ1 active type, current rating, diode polarity, B/C/E and power.
One button appears permanently pressed.Check the contact pair of the four-leg button and its assigned GPIO.
Holding a button produces no further action.A stable release followed by another press is required.

Independent challenges

  1. Change CLEAR_MS from 2000 to 5000. Find the earliest monitoring start if ARM qualifies at 70 s and PIR remains continuously LOW.
  2. Change the alarm to 100 ms of sound and 900 ms of silence while keeping its one-second cycle.
  3. Predict the counter after this sequence: first trigger, ACK, more motion, ARM to disarm, a new ARM cycle and a second trigger. Explain what reset changes.

Worked solutions

For the first challenge, set constexpr uint32_t CLEAR_MS = 5000;. The exit delay finishes at approximately 80 s. A fresh five-second interval begins with the first LOW observed in WAIT_CLEAR, so monitoring starts at approximately 85 s. It does not start at 80 s merely because PIR was already quiet during the exit delay.

In updateOutputs(), change the buzzer condition from elapsed % 1000UL < 200UL to elapsed % 1000UL < 100UL. The period remains 1000 ms, while the audible duty fraction decreases from 20% to 10%. This changes the active buzzer's on/off rhythm, not its internal tone frequency.

The first trigger gives a count of 1. ACK and further motion leave it unchanged. ARM clears the active alarm state but does not reset the counter. After deliberate rearming and the second trigger, the count is 2. Reset clears the RAM counter to 0 and restarts WARMUP.

Check your understanding

What exactly does ACK acknowledge? That the user has accepted the notification. It does not certify that the area is quiet or that the cause has been removed.

Why does WAIT_CLEAR follow the exit delay? Monitoring must begin only after a separate uninterrupted quiet interval following the opportunity to move away.

Can LOW clear a latched alarm by itself? No. The program retains the alarm state until the appropriate user command.

Which command wins in the same iteration: ARM or ACK? ARM/DISARM. During an alarm, this fully disarms the active state.

Why are there two debouncers? Each button has independent contact bounce and its own acceptance time.

Primary sources

Downloads