SGP Official store ↗
Blog / Projects / CSI motion alarm

Build a Wi-Fi CSI motion alarm with an ESP32

No camera, no PIR dome, no extra sensor. Just one ESP32 on your own Wi-Fi, learning what an empty room looks like and shouting when that changes. This is a full build: baseline, a motion score from CSI variance, a threshold with debounce, and a buzzer. The code compiles as written.

Published 20 Oct 2026Read 12 minLevel IntermediateTopic CSI alarm
Wi-Fi CSI motion alarm triggering on an ESP32 Watch · Wi-Fi alarm demo
VIDEO — Our CSI alarm in action: the room is learned while empty, then the buzzer fires the moment someone walks in.

This build follows directly from our CSI capture guide. There we turned raw Wi-Fi packets into one energy number per packet. Here we turn a stream of those numbers into a decision: is the room empty, or did something just move. If you have not captured CSI before, read that guide first, then come back.

01How the alarm works

The logic is deliberately simple, because simple is what stays reliable. The board joins your own Wi-Fi so there is a link to measure. For a short warm-up it watches an empty, still room and records how calm the signal is. That calm level becomes the baseline. After that, it keeps measuring how much the signal is churning, and when the churn rises well above the baseline it declares motion, holds the alarm for a few seconds so it does not chatter, and drives a buzzer.

Everything runs on the board. No cloud, no camera, no image of anyone. It is a presence and motion trigger for a space you own.

02Learn the empty room

CSI is never perfectly still, even in an empty room. There is always a little noise from the radio and the environment. The whole trick of a good detector is to measure that resting level first, then react only to changes that clearly exceed it. So the alarm begins with a warm-up window, about fifteen seconds by default, during which the room must be empty and nobody should walk past.

Keep out during warm-upWhatever the board sees during warm-up becomes normal. If you are moving in the room while it learns, it will treat your movement as the quiet baseline and then under-react later. Start it, step out, and let it settle.

03From CSI to a motion score

A single energy value jumps around from packet to packet, so we do not compare raw energy. Instead we look at how variable the recent energy is. A still room gives a low, steady variance. A person moving stirs the signal, and the variance climbs. That variance is our motion score: one number that is low when nothing happens and high when it does.

score time threshold motion low, steady variance = empty · a spike over the dashed line = alarm
FIG 1 — The motion score is the recent variance of CSI energy. It sits in a quiet band when the room is empty and spikes over the threshold when someone moves.

The threshold is set from the baseline: a multiple of the quiet variance, so a louder room automatically needs a bigger movement to trip. That keeps one set of numbers working across different rooms without hand-tuning for each.

04Threshold, debounce and the buzzer

Three small details make the difference between a toy and something you would actually leave running:

05The full sketch

Wire an active buzzer between GPIO 4 and ground, put your own network details in, and flash it. Then follow the steps below.

  1. Connect the buzzer. Active buzzer positive to GPIO 4, negative to GND. An active buzzer sounds whenever the pin is HIGH.
  2. Set your network. Put your own SSID and password in the two constants. Use only a network you own.
  3. Flash and leave the room. Open the serial monitor at 115200, start the board, and step out during the warm-up.
  4. Test it. After the baseline prints, walk back in. The score should spike and the buzzer should sound.
  5. Tune. If it is twitchy or deaf, adjust the trigger multiplier and window, as described in the next section.
csi_alarm.ino · Arduino-ESP32 3.x
// Camera-free Wi-Fi CSI motion alarm for ESP32 / ESP32-S3 / ESP32-C.
// Learns an empty room, then sounds a buzzer when CSI variance spikes.
#include <WiFi.h>
#include "esp_wifi.h"

const char* SSID = "YOUR_WIFI";        // your own network
const char* PASS = "YOUR_PASSWORD";

const int      BUZZER_PIN = 4;       // active buzzer: HIGH = on
const uint32_t WARMUP_MS  = 15000;   // learn the empty room for 15 s
const int      WIN        = 32;      // samples in the motion window
const float    TRIGGER_K  = 6.0;     // times the baseline before we alarm
const uint32_t HOLD_MS    = 4000;    // keep alarm on this long (debounce)

volatile double   gEnergy = 0;        // latest per-packet CSI energy
volatile uint32_t gSeq    = 0;        // increments on every CSI record

// Wi-Fi task: turn one packet into one energy value. Keep it short.
static void onCsi(void* ctx, wifi_csi_info_t* info) {
  if (!info || !info->buf) return;
  int8_t* d = info->buf;
  int n = info->len / 2;
  double e = 0;
  for (int i = 0; i < n; i++) {
    int imag = d[i*2], real = d[i*2+1];
    e += (double)(real*real + imag*imag);
  }
  gEnergy = e; gSeq++;
}

double win[WIN]; int widx = 0; bool wfull = false;

// Variance of the recent energy values = our motion score.
double motionScore() {
  int c = wfull ? WIN : widx;
  if (c < 2) return 0;
  double m = 0; for (int i=0;i<c;i++) m += win[i]; m /= c;
  double v = 0; for (int i=0;i<c;i++){ double dd=win[i]-m; v += dd*dd; }
  return v / c;
}

double baseline = 0, baseSum = 0; int baseCnt = 0;
bool learned = false;
uint32_t startMs = 0, alarmUntil = 0, lastSeq = 0;

void setup() {
  Serial.begin(115200); delay(300);
  pinMode(BUZZER_PIN, OUTPUT); digitalWrite(BUZZER_PIN, LOW);

  WiFi.mode(WIFI_STA); WiFi.begin(SSID, PASS);
  while (WiFi.status() != WL_CONNECTED) { delay(400); Serial.print("."); }
  Serial.println("\nconnected");

  wifi_csi_config_t cfg = {
    .lltf_en=true, .htltf_en=true, .stbc_htltf2_en=true,
    .ltf_merge_en=true, .channel_filter_en=false,
    .manu_scale=false, .shift=0 };
  esp_wifi_set_csi_config(&cfg);
  esp_wifi_set_csi_rx_cb(&onCsi, NULL);
  esp_wifi_set_csi(true);

  startMs = millis();
  Serial.println("Warming up: keep the room empty and still...");
}

void loop() {
  // pull the newest energy sample into the window
  if (gSeq != lastSeq) {
    lastSeq = gSeq;
    win[widx] = gEnergy; widx = (widx+1) % WIN; if (widx==0) wfull=true;
  }
  double score = motionScore();
  uint32_t now = millis();

  if (!learned) {
    if (now - startMs < WARMUP_MS) { baseSum += score; baseCnt++; }
    else { baseline = baseCnt ? baseSum/baseCnt : score; learned = true;
           Serial.printf("baseline = %.0f  (armed)\n", baseline); }
  } else {
    double thr = baseline * TRIGGER_K + 1.0;   // +1 guards a near-zero baseline
    if (score > thr) alarmUntil = now + HOLD_MS;  // (re)trigger and debounce
    bool on = now < alarmUntil;
    digitalWrite(BUZZER_PIN, on ? HIGH : LOW);
    Serial.printf("score=%.0f thr=%.0f %s\n", score, thr, on ? "ALARM" : "quiet");
  }
  delay(40);  // ~25 Hz decision rate
}
Few samples?Remember CSI only arrives with packets. If the window fills slowly, keep a little traffic on the link, for example a continuous ping to the board, so the detector has a steady stream to work with.

06Tuning it for your room

Out of the box the alarm works, but every room is different. Three dials cover almost everything:

DialIf it fires too easilyIf it misses people
TRIGGER_KRaise it (8, 10)Lower it (4, 5)
WINIncrease for a calmer scoreDecrease for a faster reaction
WARMUP_MSLengthen for a steadier baselineMake sure the room was truly empty

The most common cause of false alarms is something in the room that moves on its own. A fan, an air-conditioning vent, a curtain in a draft or a pet will all stir the channel. Point the link across the area you care about rather than at a vent, raise TRIGGER_K so small motion is ignored, and if pets are the issue, accept that a single CSI link cannot reliably tell a cat from a person, so pair it with another sensor for pet-immune alarming. We compare CSI with dedicated presence hardware in mmWave radar vs PIR.

SGP Card Mini ESP32-S3 board running the CSI motion alarm
The SGP Card Mini runs the alarm over USB-C. Add a small active buzzer to the GPIO header and it is a self-contained, camera-free sensor.

07Make it yours

The buzzer is only a placeholder for the moment the score crosses the threshold. From that exact point in the code you can do anything the board can do. Because it is already on your Wi-Fi, the natural upgrades are a push notification through your home automation system, a webhook to a service you run, or an MQTT message other devices listen for. You could also log the score to build a picture of when a room is normally used.

Keep the framing in mind: this is a sensor for your own space. It tells you a room you own is occupied, with no camera and no recording of anyone. That is the strength of Wi-Fi sensing, and the reason to build it this way. For the theory behind all of this, circle back to the complete CSI guide.

08FAQ

How long does the warm-up take?

The sketch learns the empty room for about fifteen seconds by default, and you can make it longer for a more stable baseline. During warm-up the room must be empty and still, because whatever it sees then becomes the quiet reference that every later reading is compared against.

What causes false alarms, and how do I stop them?

Anything that moves the radio channel can trip it: a fan, a pet, a curtain in a draft, or a phone waking up nearby. Raise the trigger multiplier so small movements are ignored, average over a longer window, and place the board and router so the air path across the room, not a vent, is what the link covers.

Can the alarm send a phone alert instead of a buzzer?

Yes. The buzzer line is just one action taken when the score crosses the threshold. Because the board is already on your Wi-Fi, you can replace or add to it: send a message to a home automation system, call a webhook, or publish over MQTT, all from the same trigger point in the code.

Does the ESP32 need internet for this alarm?

No. It only needs to join your own local access point so there is a Wi-Fi link to measure. The detection runs entirely on the board, and the buzzer works with no internet at all. Internet is only needed if you choose to add remote alerts that leave your network.

SGP Lab — written by the team that designs and builds SGP hardware in Europe. We test every example on our own boards before publishing.

SGP devices are sold only at our official store.

sgpstore.net ↗