Skip to content

Motion-Triggered Alarm System

Works with

Any CircuitPython board with I2C and PWM — Feather M0/M4, ItsyBitsy M4, Trinket M0, RP2040 boards, Circuit Playground Express/Bluefruit

Libraries used: adafruit_lis3dh · adafruit_led_animation · simpleio


What you will build

A small box that sits quietly until something disturbs it — a tap, a knock, or someone picking it up. The moment the LIS3DH accelerometer detects movement above a threshold, NeoPixels erupt into a strobing red animation and a buzzer screams a repeating tone. Reset it with the reset button or add a key switch for a more theatrical effect.

This is not a toy sensor demo. It is a complete, deployed alarm — the kind of thing you could tape inside a locker, mount on a door, or install on a project box at a science fair to deter tampering.


Parts list

Part Notes
CircuitPython board with I2C and PWM Feather M4 Express recommended
LIS3DH accelerometer breakout Adafruit #2809
NeoPixel strip or ring 8–16 pixels works well
Passive piezo buzzer 3–5V, any generic
STEMMA QT / Qwiic cable or breadboard jumpers For I2C wiring
Small enclosure or project box Optional but satisfying
USB cable + power source

Wiring

graph TD
    MCU["CircuitPython Board"]
    LIS["LIS3DH\nAccelerometer"]
    NEO["NeoPixel Strip"]
    BUZ["Piezo Buzzer"]

    MCU -- "3.3V" --> LIS
    MCU -- "GND" --> LIS
    MCU -- "SCL (I2C)" --> LIS
    MCU -- "SDA (I2C)" --> LIS

    MCU -- "5V" --> NEO
    MCU -- "GND" --> NEO
    MCU -- "D5 (data)" --> NEO

    MCU -- "D9 (PWM)" --> BUZ
    MCU -- "GND" --> BUZ

Tip

The LIS3DH can also connect via STEMMA QT if your board has a QT port — no breadboard needed.


Complete code

import time
import board
import busio
import neopixel
import simpleio
import adafruit_lis3dh
from adafruit_led_animation.animation.strobe import Strobe
from adafruit_led_animation.color import RED, BLACK
from adafruit_led_animation.sequence import AnimationSequence

# --- Hardware setup ---
i2c = busio.I2C(board.SCL, board.SDA)
lis3dh = adafruit_lis3dh.LIS3DH_I2C(i2c)

# Sensitivity: 0 = most sensitive, higher = less sensitive
# Try values between 10 and 40 — start at 20 and adjust
MOTION_THRESHOLD = 20
lis3dh.set_tap(1, MOTION_THRESHOLD)  # single tap detection

PIXEL_PIN = board.D5
NUM_PIXELS = 8
BUZZER_PIN = board.D9

pixels = neopixel.NeoPixel(PIXEL_PIN, NUM_PIXELS, brightness=1.0, auto_write=False)

# LED animation
strobe = Strobe(pixels, speed=0.05, color=RED, period=0.5)

# Alarm state
alarm_active = False
alarm_start = 0
ALARM_DURATION = 5  # seconds


def trigger_alarm():
    global alarm_active, alarm_start
    alarm_active = True
    alarm_start = time.monotonic()
    print("ALARM TRIGGERED")


def silence_alarm():
    global alarm_active
    alarm_active = False
    pixels.fill(BLACK)
    pixels.show()


print("Alarm system armed. Waiting...")

while True:
    # Check for tap / motion
    if lis3dh.tapped and not alarm_active:
        trigger_alarm()

    if alarm_active:
        elapsed = time.monotonic() - alarm_start

        # Drive the strobe animation
        strobe.animate()

        # Buzzer: 880 Hz tone in 0.1 s bursts
        simpleio.tone(BUZZER_PIN, 880, duration=0.1)
        time.sleep(0.05)
        simpleio.tone(BUZZER_PIN, 1100, duration=0.1)
        time.sleep(0.05)

        # Auto-silence after ALARM_DURATION seconds
        if elapsed >= ALARM_DURATION:
            silence_alarm()
            print("Alarm silenced. Re-arming...")
            time.sleep(2)  # brief pause before re-arming

    else:
        # Idle: check sensor frequently
        time.sleep(0.05)

How it works

Motion detection with LIS3DH

The LIS3DH is a three-axis accelerometer that can detect both continuous motion and discrete tap events in hardware. Calling set_tap(1, threshold) programs the chip to raise an interrupt flag whenever a single-tap acceleration spike exceeds the threshold value. In CircuitPython, you read that flag by checking lis3dh.tapped — it returns True once per tap event and then resets. The threshold is in raw units from 0 to 127; lower values mean the sensor fires on gentler disturbances. A value around 20 detects a firm tap on the desk but ignores ordinary vibration.

LED animation with adafruit_led_animation

The adafruit_led_animation library manages animations as objects you create once and then call .animate() on inside your main loop. The Strobe animation flashes the pixels on and off at a rate controlled by speed and period. Because .animate() is non-blocking and tracks its own timing with time.monotonic(), you can call it on every iteration of the loop without using time.sleep() — which is important here because the buzzer also needs to run at the same time.

Tone generation with simpleio

simpleio.tone(pin, frequency, duration) drives a PWM signal at the given frequency for the given duration in seconds. It blocks for that duration, which is why the tones are kept short (0.1 s each). The alternating 880 Hz and 1100 Hz tones create a classic two-note alarm pattern. Any passive piezo buzzer will work; active buzzers have a fixed internal frequency and will not respond to the frequency argument.


Remix ideas

Remix idea

Send an SMS alert over WiFi. When the alarm triggers, post to Adafruit IO over WiFi and use IFTTT or Adafruit IO actions to send yourself a text message. See Adafruit IO Basics to get started.

Remix idea

Silence the alarm from your phone. Add BLE so you can send a "disarm" command wirelessly. The BLE Keyboard / HID builder shows you how to set up a BLE UART service that you can repurpose for any command.

Remix idea

Start simpler. If this project feels like a lot at once, the Motion Alarm builder page walks through just the LIS3DH side of things — no animation library, no buzzer — so you can get comfortable with the sensor first.


Go deeper

All three libraries used in this project have their own reference pages with additional examples, configuration options, and troubleshooting tips.