Brody's Builds
← All projects
Keypad Motion Alarm
ArduinoMedium1 to 2 hours

Keypad Motion Alarm

The alarm without Wi-Fi: a motion sensor, a 4x4 keypad for the code and a loud siren, all on a regular Arduino UNO.

Board: Arduino UNO (Elegoo UNO R3)

What you need

What it is

A room alarm you control with a keypad code. Type the code to arm it and walk out. If someone comes in, it beeps a warning for 15 seconds, and if they don't type the code, a loud siren goes off until they do.

This is the first version of the alarm, on a regular UNO. Later I moved it to a Wi-Fi board and added a phone app and phone alerts: see the Wi-Fi Motion Alarm. The wiring for the sensor and keypad is the same, so you can start here and upgrade later.

How it works

  • The motion sensor sees body heat moving. It needs about a minute to settle after power-on, then it beeps twice: ready.
  • The keypad has 4 rows and 4 columns of wires. The program powers one row at a time and checks which column answers, to find the key.
  • No Enter key: it reacts as soon as the last keys you typed match the code.
  • A louder siren: instead of driving one leg of the buzzer, the program flips both legs in opposite directions, so the buzzer sees +5 V and then −5 V. That's twice the push, and the sound sweeps from 1.8 to 3 kHz where the buzzer is loudest.
  • It remembers its state, so unplugging it or pressing reset doesn't turn it off.
StateWhat happens
OFFIgnores movement. Type the code to arm.
ARMINGBeeps every second for 15 s so you can leave.
ARMEDThe L light blinks every 2 s. Movement starts the countdown.
COUNTDOWN15 s of warning beeps. Type the code to stop it.
ALARMSiren until the code is typed.

Build it

1Upload the code

Upload the code below (see Getting started).

2Wire it

No breadboard: everything goes straight into the Arduino.

The whole alarm: keypad on 12 to 5, motion sensor on 4, 3, 2, buzzer in A0 to A2
The whole alarm: keypad on 12 to 5, motion sensor on 4, 3, 2, buzzer in A0 to A2
  • Motion sensor: three female-to-male wires, GND → 4, OUT → 3, VCC → 2. The sensor uses so little power that pins 2 and 4 can power it.
  • Keypad: with the keys facing up, its 8 holes go left to right into pins 12, 11, 10, 9, 8, 7, 6, 5 (8 male-to-male wires).
  • Buzzer module: plug its 3 pins straight into A0, A1, A2, either way round.

Tip: If the motion sensor's VCC and GND end up the wrong way round, it says "motion" all the time. The program spots this and swaps pins 2 and 4 for you.

Using it

  • Arm / turn off: type the code.
  • Change the code: while it's OFF, press *, type a new code (4 to 8 keys), press #, type it again, press #.
  • Serial Monitor (9600 baud): ? status, t test siren, m mute, k show keys as you press them, d turn off, r reset the code to 1234.

If it doesn't work

  • It keeps going off a second or two after the siren stops: a loud sound stopping suddenly makes these sensors twitch. The program ignores the sensor for 5 seconds after any sound.
  • Wrong keys: type k and press keys. If pressing 1 shows something else, the keypad wires are in a different order.
  • Forgot the code: type r in the Serial Monitor to reset it to 1234.
  • Don't turn the sensitivity knob down (the one near the edge of the sensor), or it won't see across the room. The other knob (time) is fine to turn down.

The code

One file, no libraries. The starting code is 1234; change it from the keypad once it's running.

MotionAlarm.ino · 559 lines Download
// Motion Alarm with keypad: HC-SR501 motion sensor + 4x4 keypad + buzzer module// + the board's "L" light.//// Wiring://   Motion sensor (3 female-to-male wires): GND -> pin 4, OUT -> pin 3, VCC -> pin 2//     (it uses so little power that pin 2 can power it and pin 4 can be its ground;//      if VCC and GND are the other way round, the program notices and swaps them)//   Keypad (8 male-to-male wires), its 8 holes left to right with the keys facing up://     12, 11, 10, 9, 8, 7, 6, 5   (rows on 12-9, columns on 8-5)//   Buzzer module (passive): plugged straight into A0, A1, A2 (either way round)//   Light: the built-in "L" light (pin 13), no wiring//// How it works://   OFF (disarmed)  - ignores movement. Type the code to arm it.//   ARMING          - beeps every second for EXIT_MS so you can walk away.//                     Typing the code again cancels.//   ARMED           - L blinks every 2 s. Movement starts the countdown.//                     Typing the code turns it off.//   COUNTDOWN       - warning beeps for ENTRY_MS (faster near the end): type//                     the code to turn it off before the siren starts.//   ALARM           - siren non-stop until the code is typed.//   No Enter key needed: it reacts as soon as the last keys typed match the code.//   The state is saved in EEPROM, so unplugging or resetting doesn't turn it off.//   Change the code: while OFF, press *, type the new code (4-8 keys), press #,//   type it again, press #. Default code: 1234.//// Loudness: Timer1 flips the buzzer's two ends in opposite directions (push-pull),// so it sees +5V then -5V, and the wail sweeps 1.8-3 kHz where it's loudest.//// The sudden stop of a loud sound disturbs the sensor, and it reports "motion"// a second or two later. So the sensor is ignored for DEAF_MS after any sound.//// Serial (9600): "?" = status, "t" = test siren 3 s, "m" = mute/unmute,//   "k" = show which keys are pressed (for checking the wiring),//   "d" = turn off (rescue), "r" = reset the code to the default (rescue)#include <EEPROM.h>const byte PIR_PIN = 3;byte pirVcc = 2;           // pins 2 and 4 trade places if the sensor'sbyte pirGnd = 4;           // power wires turn out to be the other way roundconst byte BUZ_MID = A1;   // buzzer's middle pin (power, on modules that use it)const byte BUZ_BITS = _BV(PC0) | _BV(PC2);   // buzzer ends: A0 and A2const byte LIGHT = 13;const byte ROW_PINS[4] = {12, 11, 10, 9};const byte COL_PINS[4] = {8, 7, 6, 5};const char KEYS[4][4] = {  {'1', '2', '3', 'A'},  {'4', '5', '6', 'B'},  {'7', '8', '9', 'C'},  {'*', '0', '#', 'D'},};const unsigned long WARMUP_MS = 60000UL;const unsigned long EXIT_MS = 15000UL;     // time to walk away after armingconst unsigned long ENTRY_MS = 15000UL;    // time to type the code after movementconst unsigned long DEAF_MS = 5000UL;      // ignore the sensor this long after a soundconst unsigned long CHANGE_TIMEOUT_MS = 30000UL;const unsigned int WAIL_LOW = 1800;const unsigned int WAIL_HIGH = 3000;const unsigned long WAIL_MS = 1000UL;      // one up-and-down sweepconst byte MAX_CODE = 8;const char DEFAULT_CODE[] = "1234";// EEPROM layoutconst int EE_MUTE = 0;const int EE_STATE = 1;const int EE_CODE_MAGIC = 2;const int EE_CODE_LEN = 3;const int EE_CODE = 4;          // up to 8 bytesconst byte MUTE_MAGIC = 0xA5;const byte CODE_MAGIC = 0x5C;enum State : byte { DISARMED = 0, ARMED = 1, ALARM = 2, ARMING = 3, ENTRY = 4 };State state = DISARMED;bool muted;bool sounding = false;bool echoKeys = false;unsigned long stateStart = 0;unsigned long deafUntil = 0;unsigned long testUntil = 0;unsigned long motionCount = 0;unsigned long warmStart = 0;bool powerConfirmed = false; // the sensor has shown it's powered the right way roundbyte swaps = 0;unsigned long lastPowerCheck = 0;char code[MAX_CODE];byte codeLen;char typed[MAX_CODE];        // the last keys typed (rolling)byte typedLen = 0;byte changeStep = 0;         // 0 = not changing, 1 = first entry, 2 = confirmchar newCode[MAX_CODE];byte newLen = 0;char firstCode[MAX_CODE];byte firstLen = 0;unsigned long lastKeyAt = 0;// ---------- Sound: Timer1 flips A0 and A2 together ----------ISR(TIMER1_COMPA_vect) {  PINC = BUZ_BITS;          // writing 1s to PINC toggles those pins}void soundOn(unsigned int hz) {  unsigned int top = 1000000UL / hz - 1;   // Timer1 ticks at 2 MHz, toggling twice per cycle  noInterrupts();  OCR1A = top;  if (TCNT1 > top) TCNT1 = 0;  if (!sounding) {    PORTC = (PORTC & ~BUZ_BITS) | _BV(PC0);   // start with A0 HIGH, A2 LOW    TCNT1 = 0;    TIFR1 = _BV(OCF1A);    TIMSK1 |= _BV(OCIE1A);    sounding = true;  }  interrupts();}void soundOff() {  noInterrupts();  TIMSK1 &= ~_BV(OCIE1A);  PORTC &= ~BUZ_BITS;       // both ends LOW: nothing through the buzzer  sounding = false;  interrupts();  deafUntil = millis() + DEAF_MS;}void tone2(unsigned int hz, unsigned int ms) {  if (!muted) soundOn(hz);  digitalWrite(LIGHT, HIGH);  delay(ms);  soundOff();  digitalWrite(LIGHT, LOW);}void click()    { tone2(2400, 25); }void twoBeeps() { tone2(2400, 80); delay(80); tone2(2400, 80); }void goodTone() { tone2(1800, 100); tone2(2700, 160); }void badTone()  { tone2(500, 450); }// Called every loop while the siren is on: wail up and down, flash the light.void updateSiren(unsigned long now) {  unsigned long t = (now - stateStart) % WAIL_MS;  unsigned long half = WAIL_MS / 2;  unsigned int span = WAIL_HIGH - WAIL_LOW;  unsigned int hz = t < half ? WAIL_LOW + span * t / half                             : WAIL_HIGH - span * (t - half) / half;  if (muted) { if (sounding) soundOff(); }  else soundOn(hz);  digitalWrite(LIGHT, ((now - stateStart) / 100) % 2 == 0);}// ---------- State ----------void setState(State s) {  state = s;  stateStart = millis();  EEPROM.update(EE_STATE, s == ARMING ? ARMED : s);   // power loss while arming = armed  switch (s) {    case DISARMED: Serial.println(F("OFF - disarmed")); break;    case ARMING:   Serial.println(F("Arming - leave within 15 s")); break;    case ARMED:    Serial.println(F("ARMED - watching for motion")); break;    case ENTRY:    Serial.println(F("Countdown - 15 s to type the code")); break;    case ALARM:    Serial.println(F("ALARM - siren on")); break;  }}bool sensorWarm() {  return millis() - warmStart >= WARMUP_MS;}// ---------- Code ----------void loadCode() {  if (EEPROM.read(EE_CODE_MAGIC) == CODE_MAGIC) {    codeLen = EEPROM.read(EE_CODE_LEN);    if (codeLen >= 4 && codeLen <= MAX_CODE) {      for (byte i = 0; i < codeLen; i++) code[i] = EEPROM.read(EE_CODE + i);      return;    }  }  codeLen = strlen(DEFAULT_CODE);  memcpy(code, DEFAULT_CODE, codeLen);}void saveCode(const char *c, byte len) {  memcpy(code, c, len);  codeLen = len;  EEPROM.update(EE_CODE_LEN, len);  for (byte i = 0; i < len; i++) EEPROM.update(EE_CODE + i, c[i]);  EEPROM.update(EE_CODE_MAGIC, CODE_MAGIC);}bool typedMatches() {  if (typedLen < codeLen) return false;  return memcmp(typed + typedLen - codeLen, code, codeLen) == 0;}void codeAccepted() {  switch (state) {    case DISARMED:      goodTone();      setState(ARMING);      break;    case ARMING:    case ARMED:    case ENTRY:      goodTone();      setState(DISARMED);      break;    case ALARM:      soundOff();      digitalWrite(LIGHT, LOW);      delay(300);      goodTone();      setState(DISARMED);      break;  }}void handleChange(char k) {  if (k == '*') {    changeStep = 0;    badTone();    Serial.println(F("Code change cancelled"));    return;  }  if (k != '#') {    if (newLen >= MAX_CODE) {      changeStep = 0;      badTone();      Serial.println(F("Code change cancelled - too long (8 keys max)"));      return;    }    newCode[newLen++] = k;    click();    return;  }  if (changeStep == 1) {    if (newLen < 4) {      changeStep = 0;      badTone();      Serial.println(F("Code change cancelled - too short (4 keys min)"));      return;    }    memcpy(firstCode, newCode, newLen);    firstLen = newLen;    newLen = 0;    changeStep = 2;    twoBeeps();    Serial.println(F("Type the new code again, then #"));  } else {    changeStep = 0;    if (newLen == firstLen && memcmp(newCode, firstCode, newLen) == 0) {      saveCode(newCode, newLen);      goodTone();      Serial.println(F("New code saved"));    } else {      badTone();      Serial.println(F("The two codes didn't match - kept the old code"));    }  }}void onKey(char k) {  lastKeyAt = millis();  if (echoKeys) { Serial.print(F("Key: ")); Serial.println(k); }  if (changeStep) { handleChange(k); return; }  if (k == '*' && state == DISARMED) {    changeStep = 1;    newLen = 0;    twoBeeps();    Serial.println(F("Changing the code: type the new one, then #"));    return;  }  if (state != ALARM) click();  if (k == '*' || k == '#') {     // clears what's been typed    typedLen = 0;    return;  }  if (typedLen == MAX_CODE) {    memmove(typed, typed + 1, MAX_CODE - 1);    typedLen--;  }  typed[typedLen++] = k;  if (typedMatches()) {    typedLen = 0;    codeAccepted();  }}// ---------- Keypad ----------char scanKeypad() {  char found = 0;  for (byte r = 0; r < 4; r++) {    pinMode(ROW_PINS[r], OUTPUT);    digitalWrite(ROW_PINS[r], LOW);    delayMicroseconds(5);    for (byte c = 0; c < 4; c++)      if (!digitalRead(COL_PINS[c])) found = KEYS[r][c];    pinMode(ROW_PINS[r], INPUT_PULLUP);  }  return found;}void pollKeypad() {  static unsigned long lastScan = 0;  static char lastRead = 0, stableKey = 0;  static byte same = 0;  if (millis() - lastScan < 10) return;  lastScan = millis();  char k = scanKeypad();  if (k != lastRead) { lastRead = k; same = 0; return; }  if (same < 3) same++;  if (same == 2 && k != stableKey) {   // the same for 30 ms: a real press    stableKey = k;    if (k) onKey(k);  }}// ---------- Motion sensor power and wiring checks ----------void powerSensor() {  pinMode(pirGnd, OUTPUT);  digitalWrite(pirGnd, LOW);  pinMode(pirVcc, OUTPUT);  digitalWrite(pirVcc, HIGH);}bool powerPinsOk() {  return digitalRead(pirVcc) == HIGH && digitalRead(pirGnd) == LOW;}// Tells "something is holding OUT HIGH" apart from "nothing is connected", which// both read HIGH with the pull-up: pull the pin LOW for a moment and let go.// Something driving it brings it straight back up; a loose wire stays LOW.bool outDriven() {  digitalWrite(PIR_PIN, LOW);  pinMode(PIR_PIN, OUTPUT);  delayMicroseconds(10);  pinMode(PIR_PIN, INPUT);  delayMicroseconds(5);  bool driven = digitalRead(PIR_PIN);  pinMode(PIR_PIN, INPUT_PULLUP);  return driven;}// A sensor with VCC and GND swapped isn't damaged (it has a protection diode),// but its GND pin is then at 5V and leaks through the chip to OUT, so OUT looks// like non-stop motion. To spot that, cut the power for half a millisecond:// a sensor powered the right way keeps going on its capacitors, while the// back-to-front one drops OUT straight away. If that happens, swap pins 2 and 4.void checkPower() {  if (!digitalRead(PIR_PIN)) {      // only a properly powered sensor can pull OUT LOW    powerConfirmed = true;    Serial.println(F("Sensor power: OK"));    return;  }  if (!outDriven()) return;         // loose OUT wire: nothing to learn yet  digitalWrite(pirVcc, LOW);  delayMicroseconds(500);  bool held = outDriven();  digitalWrite(pirVcc, HIGH);  if (held) {    powerConfirmed = true;    Serial.println(F("Sensor power: OK"));    return;  }  byte t = pirVcc; pirVcc = pirGnd; pirGnd = t;  swaps++;  powerSensor();  delay(2);  if (swaps >= 2 || !powerPinsOk()) {    if (swaps < 2 || !powerPinsOk()) { t = pirVcc; pirVcc = pirGnd; pirGnd = t; powerSensor(); }    powerConfirmed = true;          // stop trying    Serial.println(F("Sensor power: couldn't work it out - please check the VCC and GND wires"));    return;  }  Serial.println(F("Sensor power: VCC and GND wires are the other way round - fixed it in the program"));  warmStart = millis();             // it has to settle again}void printSensorCheck() {  if (!powerPinsOk()) {    Serial.println(F("Sensor: power pins 2/4 are overloaded - check the VCC and GND wires"));  } else if (!digitalRead(PIR_PIN)) {    Serial.println(F("Sensor: connected, no motion"));  } else if (outDriven()) {    Serial.println(F("Sensor: OUT is HIGH (motion right now)"));  } else {    Serial.println(F("Sensor: not answering - OUT wire loose, or no power on VCC/GND"));  }  Serial.print(F("Sensor power: VCC on pin "));  Serial.print(pirVcc);  Serial.print(F(", GND on pin "));  Serial.print(pirGnd);  Serial.println(powerConfirmed ? F(" (confirmed)") : F(" (still checking)"));}void printStatus() {  static const char names[][10] = {"OFF", "ARMED", "ALARM", "ARMING", "COUNTDOWN"};  Serial.print(F("State: "));  Serial.print(names[state]);  Serial.print(sensorWarm() ? F(" | sensor ready") : F(" | sensor warming up"));  Serial.print(F(" | sound "));  Serial.print(muted ? F("OFF") : F("ON"));  Serial.print(F(" | code length "));  Serial.print(codeLen);  Serial.print(F(" | alarms so far: "));  Serial.println(motionCount);  printSensorCheck();}void handleSerial() {  while (Serial.available()) {    char c = Serial.read();    if (c == '?') {      printStatus();    } else if (c == 't') {      Serial.println(F("Test siren"));      testUntil = millis() + 3000;    } else if (c == 'm') {      muted = !muted;      EEPROM.update(EE_MUTE, muted ? MUTE_MAGIC : 0xFF);      if (muted && sounding) soundOff();      Serial.println(muted ? F("Sound OFF (light only)") : F("Sound ON"));    } else if (c == 'k') {      echoKeys = !echoKeys;      Serial.println(echoKeys ? F("Showing keys") : F("Not showing keys"));    } else if (c == 'd') {      if (sounding) soundOff();      digitalWrite(LIGHT, LOW);      setState(DISARMED);    } else if (c == 'r') {      saveCode(DEFAULT_CODE, strlen(DEFAULT_CODE));      Serial.println(F("Code reset to the default"));    }  }}void setup() {  Serial.begin(9600);  powerSensor();  pinMode(PIR_PIN, INPUT_PULLUP);   // reads HIGH if the OUT wire is missing, not random  pinMode(LIGHT, OUTPUT);  for (byte i = 0; i < 4; i++) {    pinMode(ROW_PINS[i], INPUT_PULLUP);    pinMode(COL_PINS[i], INPUT_PULLUP);  }  pinMode(A0, OUTPUT);  pinMode(A2, OUTPUT);  PORTC &= ~BUZ_BITS;  // Power the middle pin, unless it's joined to an end (then it's not a power pin).  pinMode(BUZ_MID, INPUT_PULLUP);  delay(5);  if (digitalRead(BUZ_MID)) {    pinMode(BUZ_MID, OUTPUT);    digitalWrite(BUZ_MID, HIGH);  } else {    pinMode(BUZ_MID, INPUT);  }  TCCR1A = 0;  TCCR1B = _BV(WGM12) | _BV(CS11);  // Timer1: CTC mode, 16 MHz / 8 = 2 MHz  muted = EEPROM.read(EE_MUTE) == MUTE_MAGIC;  loadCode();  warmStart = millis();  Serial.println(F("Motion alarm with keypad starting"));  byte saved = EEPROM.read(EE_STATE);  if (saved == ALARM) {    setState(ALARM);                // it was going off: keep going until the code    Serial.println(F("MOTION (still going off from before)"));  } else if (saved == ENTRY) {    setState(ENTRY);                // unplugged during the countdown: start it again  } else if (saved == ARMED) {    setState(ARMING);    stateStart = millis() - EXIT_MS;   // no walk-away time needed, just the warm-up    twoBeeps();  } else {    setState(DISARMED);    twoBeeps();  }}void loop() {  handleSerial();  pollKeypad();  unsigned long now = millis();  if (!powerConfirmed && now - lastPowerCheck >= 2000UL) {    lastPowerCheck = now;    checkPower();    now = millis();  }  if (changeStep && now - lastKeyAt >= CHANGE_TIMEOUT_MS) {    changeStep = 0;    badTone();    Serial.println(F("Code change cancelled - took too long"));  }  switch (state) {    case DISARMED:      if ((long)(testUntil - now) > 0) updateSiren(now);      else if (sounding) { soundOff(); digitalWrite(LIGHT, LOW); }      break;    case ARMING: {      unsigned long t = now - stateStart;      static unsigned long lastTick = 0;      if (t < EXIT_MS && now - lastTick >= 1000) {        lastTick = now;        tone2(2400, 40);      }      if (t >= EXIT_MS && sensorWarm()) {        tone2(2700, 300);        setState(ARMED);      }      break;    }    case ARMED:      digitalWrite(LIGHT, (now - stateStart) % 2000 < 60);      if ((long)(now - deafUntil) >= 0 && digitalRead(PIR_PIN) && outDriven()) {        motionCount++;        Serial.print(F("MOTION #"));        Serial.println(motionCount);        setState(ENTRY);      }      break;    case ENTRY: {      // A warning beep every second, every half second in the last 5 s.      unsigned long t = now - stateStart;      static unsigned long lastBeep = 0;      unsigned long gap = t < ENTRY_MS - 5000 ? 1000 : 500;      if (t >= ENTRY_MS) {        setState(ALARM);      } else if (now - lastBeep >= gap || t < 50) {        lastBeep = now;        tone2(2000, 70);      }      break;    }    case ALARM:      updateSiren(now);      break;  }}

Related projects