Build a smoke alarm listener that flashes for people who can't hear it

A smoke alarm is useless if you can't hear it. This listener recognises the beep pattern of smoke and CO alarms, ignores talking and music, and flashes a bright light. It can also learn the pattern of your own alarm...
  • Level:Advanced
  • Duration: 1 h 15 min
  • Ages 12+
  • Platform: Arduino

Tools for this lesson

Helpful for this build. Open a tool to see tips and more projects that use it.

Build a smoke alarm listener that flashes for people who can't hear it – EEMENTOR STEM™ lesson

People who are deaf or hard of hearing can't hear a smoke alarm, especially at night. Special alarms with flashing lights and vibrating pads exist, but the idea behind them is simple enough to build yourself. Smoke alarms don't just beep, they beep in a fixed pattern: three beeps and a pause, again and again (called T3). Carbon monoxide alarms use four short beeps and a longer pause (T4). In this lesson the UNO listens with a sound sensor, measures every beep and every gap, and compares them with these patterns. Talking, music or a door bang don't fit, so they are ignored. When the pattern of an alarm is heard twice in a row, the white LED starts flashing like a strobe light. You can even teach it the pattern of a different alarm by playing it once.

What you'll learn

  • How to turn a sound into features: the lengths of beeps and gaps
  • How pattern recognition by template matching works, with a tolerance
  • Why asking for two matches in a row reduces false alarms
  • How a machine can learn a new pattern from one single example (one-shot learning)

What you need

  • UNO R3 board and USB cable (UNO R3 Starter Kit)
  • High-sensitivity sound sensor module (45-in-1 Sensor Kit)
  • White LED, red LED and 2 resistors, 220 Ω (UNO R3 Starter Kit)
  • Passive buzzer (UNO R3 Starter Kit)
  • 1 push button (UNO R3 Starter Kit)
  • Breadboard and jumper wires (UNO R3 Starter Kit)

Important: this is a learning project, not a certified safety device. People who can't hear alarms should use certified alarms with flashing lights or vibrating pads. Keep your normal smoke alarms. Never make smoke to test it: use the test button of your smoke alarm, or the test pattern in the sketch.

Wiring

Breadboard wiring diagram: Build a smoke alarm listener that flashes for people who can't hear it
Breadboard view. Check each connection against the wiring table below.
From To
Sound sensor G GND
Sound sensor + 5V
Sound sensor DO UNO pin 2
UNO pin 9 220 Ω resistor, then the white LED's long leg ("strobe")
UNO pin 10 220 Ω resistor, then the red LED's long leg
Both LEDs' short legs GND
Passive buzzer + UNO pin 8
Passive buzzer − GND
Push button, one leg UNO pin 4
Push button, the diagonally opposite leg GND

The sound sensor's AO pin stays unused. Before you start, turn the sensor's blue knob: in a quiet room, its small LED should be off, and it should light when you clap or talk loudly near it.

The sketch

EEPROM comes with the Arduino IDE.

#include <EEPROM.h>

const int SOUND_PIN = 2, BUZZER_PIN = 8, BUTTON_PIN = 4, STROBE_PIN = 9, RED_PIN = 10;
const int MAX_BEEPS = 6;
const unsigned long QUIET_GAP = 40;     // ms of silence = the beep has ended
const unsigned long MIN_BEEP = 40;      // shorter sounds are clicks, not beeps
const unsigned long CYCLE_END = 1000;   // 1 s of silence = the pattern is complete
const unsigned long ALERT_TIME = 60000; // flash for 1 minute (button stops it)

struct Pattern {
  byte marker;                          // 0x5A when a learned pattern is saved
  byte count;                           // number of beeps
  unsigned int beep[MAX_BEEPS];         // beep lengths in ms
  unsigned int gap[MAX_BEEPS];          // silence after each beep (not after the last)
};

// The patterns it knows: 0 = smoke alarm (T3), 1 = CO alarm (T4), 2 = learned
Pattern known[3] = {
  {0x5A, 3, {500, 500, 500}, {500, 500}},
  {0x5A, 4, {100, 100, 100, 100}, {100, 100, 100}},
  {0, 0, {0}, {0}}
};
const char* NAMES[3] = {"smoke alarm (T3)", "CO alarm (T4)", "your learned alarm"};

Pattern heard;                          // what it is hearing right now
int quietLevel;                         // sound sensor output in silence
bool inBeep = false, learning = false;
unsigned long lastSound = 0, beepStart = 0, beepEnd = 0;
int lastMatch = -1, matchCount = 0;
unsigned long lastMatchTime = 0, alertUntil = 0, testStart = 0;
int alertType = 0;
bool testing = false, toneOn = false;
bool lastButton = HIGH;
unsigned long pressStart = 0;
bool longDone = false;

bool near(unsigned int value, unsigned int target) {   // within 40 %, at least 80 ms
  long tolerance = max(target * 2L / 5, 80L);
  return labs((long)value - (long)target) <= tolerance;
}

bool matches(const Pattern& h, const Pattern& p) {
  if (p.count == 0 || h.count != p.count) return false;
  for (int i = 0; i < p.count; i++) if (!near(h.beep[i], p.beep[i])) return false;
  for (int i = 0; i < p.count - 1; i++) if (!near(h.gap[i], p.gap[i])) return false;
  return true;
}

void printHeard() {
  Serial.print("Heard ");
  Serial.print(heard.count);
  Serial.print(" beeps:");
  for (int i = 0; i < heard.count && i < MAX_BEEPS; i++) {
    Serial.print(" ");
    Serial.print(heard.beep[i]);
    if (i < heard.count - 1) {
      Serial.print(" (");
      Serial.print(heard.gap[i]);
      Serial.print(")");
    }
  }
  Serial.println();
}

void cycleFinished() {
  printHeard();
  if (heard.count > MAX_BEEPS) {        // too many beeps: speech or music
    matchCount = 0;
    return;
  }
  if (learning) {                       // one-shot learning: remember this pattern
    learning = false;
    known[2] = heard;
    known[2].marker = 0x5A;
    EEPROM.put(0, known[2]);
    Serial.println("Learned! This pattern is now saved.");
    digitalWrite(RED_PIN, LOW);
    return;
  }
  int found = -1;
  for (int p = 0; p < 3 && found < 0; p++) if (matches(heard, known[p])) found = p;
  if (found < 0) {
    matchCount = 0;
    return;
  }
  unsigned long now = millis();
  matchCount = (found == lastMatch && now - lastMatchTime < 20000) ? matchCount + 1 : 1;
  lastMatch = found;
  lastMatchTime = now;
  Serial.print("Sounds like the ");
  Serial.print(NAMES[found]);
  Serial.print(", match ");
  Serial.println(matchCount);
  if (matchCount >= 2) {                // twice in a row: sure enough
    alertType = found;
    alertUntil = now + ALERT_TIME;
    Serial.println(">>> ALARM HEARD! Flashing <<<");
  }
}

void listen(unsigned long now) {
  if (digitalRead(SOUND_PIN) != quietLevel) lastSound = now;
  bool loud = now - lastSound < QUIET_GAP;
  if (loud && !inBeep) {                // a beep starts
    inBeep = true;
    beepStart = now;
    if (heard.count > 0 && heard.count <= MAX_BEEPS) heard.gap[heard.count - 1] = now - beepEnd;
  }
  if (!loud && inBeep) {                // a beep ends
    inBeep = false;
    unsigned long length = lastSound - beepStart;
    if (length >= MIN_BEEP) {
      if (heard.count < MAX_BEEPS) heard.beep[heard.count] = length;
      if (heard.count < 250) heard.count++;
      beepEnd = lastSound;
    }
  }
  if (!inBeep && heard.count > 0 && now - beepEnd > CYCLE_END) {
    cycleFinished();
    heard.count = 0;
  }
}

void playTest(unsigned long now) {      // two rounds of T3 on the buzzer: 3 x 0.5 s, pause 1.5 s
  unsigned long t = now - testStart;
  if (t >= 8000) {
    testing = false;
    noTone(BUZZER_PIN);
    toneOn = false;
    return;
  }
  unsigned long p = t % 4000;
  bool on = p < 2500 && (p % 1000) < 500;
  if (on && !toneOn) tone(BUZZER_PIN, 3100);
  if (!on && toneOn) noTone(BUZZER_PIN);
  toneOn = on;
}

void setup() {
  Serial.begin(9600);
  pinMode(SOUND_PIN, INPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(STROBE_PIN, OUTPUT);
  pinMode(RED_PIN, OUTPUT);
  quietLevel = digitalRead(SOUND_PIN);  // keep the room quiet while it starts
  Pattern saved;
  EEPROM.get(0, saved);
  if (saved.marker == 0x5A && saved.count >= 1 && saved.count <= MAX_BEEPS) {
    known[2] = saved;
    Serial.println("Loaded your learned alarm pattern");
  }
  Serial.println("Listening. Press the button for a test, hold it 2 s to learn a new alarm.");
}

void loop() {
  unsigned long now = millis();
  listen(now);
  if (testing) playTest(now);

  // Button: short press = test pattern or stop the alert; hold 2 s = learn
  bool button = digitalRead(BUTTON_PIN);
  if (lastButton == HIGH && button == LOW) {
    pressStart = now;
    longDone = false;
  }
  if (button == LOW && !longDone && now - pressStart > 2000) {
    longDone = true;
    learning = true;
    heard.count = 0;
    Serial.println("Learn mode: make your alarm beep now (press its test button)");
  }
  if (lastButton == LOW && button == HIGH && !longDone && now - pressStart > 30) {
    if (alertUntil > now) {
      alertUntil = 0;
      Serial.println("Alert stopped");
    } else {
      testing = true;
      testStart = now;
      Serial.println("Playing a test smoke alarm pattern");
    }
  }
  lastButton = button;

  // Lights
  bool alert = alertUntil > now;
  digitalWrite(STROBE_PIN, alert && now % 500 < 60);   // short bright flashes, twice a second
  if (alert) digitalWrite(RED_PIN, alertType == 1 ? (now / 250) % 2 : HIGH);
  else digitalWrite(RED_PIN, learning);
}

Try it

  1. Upload the sketch and open the Serial Monitor at 9600 baud. Keep the room quiet for a second while it starts.
  2. Press the button briefly. The buzzer plays two rounds of the smoke alarm pattern, and the Serial Monitor shows what was heard, for example Heard 3 beeps: 498 (505) 501 (497) 502. After the second round, the white LED starts flashing.
  3. Press the button again to stop the flashing. Now talk, clap, or play music near the sensor. It hears beeps, but they don't match, so nothing flashes.
  4. If you have a smoke alarm at home, ask an adult to hold its test button with the listener nearby. Many alarms beep in the T3 pattern while the button is held.
  5. Teach it a new pattern: hold the button for 2 seconds until the red LED lights, then play one round of your alarm (for example a kitchen timer or a phone alarm sound with short beeps). It saves the pattern and recognises it from then on, even after a restart.

How it works

Pattern recognition always starts with features: a few numbers that describe what matters and leave out what doesn't. For an alarm, the loudness and the pitch don't really matter, because they change with the distance and the room. The rhythm does. So listen() turns the sound into a short list of numbers: how long each beep lasted and how long each gap was. The sound sensor only says "loud" or "quiet", and its output flickers with the sound wave itself, so the sketch treats the sound as one beep until it has been quiet for 40 ms. A full second of silence ends the pattern.

Then matches() compares the heard pattern with each template it knows. The number of beeps must be the same, and every beep and gap must be within 40 % of the template. This tolerance matters: no two alarms beep with exactly the same timing, and your sensor will measure a little differently each time. Too strict, and it misses real alarms. Too loose, and it reacts to the wrong sounds. This balance between missing things and false alarms is at the heart of every AI system that recognises things.

Hearing the pattern once could be bad luck, for example a song with three beats. Hearing it twice in a row within 20 seconds is much less likely by chance, so the alert only starts after two matches. Learning a new alarm is simply storing one heard pattern as a new template. Learning from one single example is called one-shot learning. Big AI systems usually need thousands of examples, but for a clear, repeating signal like an alarm, one good example is enough.

Try this next

  • Put a brighter light on the strobe pin: the relay module can switch a 5 V LED strip (low voltage only).
  • Add a vibration motor under a pillow for the night.
  • Rebuild it on the ESP32 and send a phone notification with ntfy as well.
  • Count how many patterns it rejected in an hour of normal family life. That number tells you how good your recogniser is.

Troubleshooting

  • It hears nothing: turn the sound sensor's knob until its LED lights when you clap, then back a little until it goes off in silence. Keep the sensor close to the alarm, a few metres at most.
  • The beeps are measured as one long beep: the sensor is too sensitive, so echoes fill the gaps. Turn the knob down a little.
  • The test pattern isn't recognised: the passive buzzer is quiet. Put it right next to the microphone. Check the numbers in the Serial Monitor: the beeps should be about 500 ms.
  • It flashes at music: some songs have very regular beeps. Make the tolerance tighter by changing 2L / 5 to 1L / 4 (25 %).

Build it yourself

Everything used in this lesson comes in one kit, with parts tested by our engineers.

UNO R3 Starter Kit | Arduino IDE Compatible | Free Tutorials $56.00

Shop the kit