Wi-Fi Motion Alarm
A home alarm with a motion sensor, a keypad code and a loud siren, plus a phone app to arm it and instant phone alerts when someone walks in.
Board: Arduino UNO R4 WiFi
What you need
What it is
A real alarm you can put in a room. Arm it with a code on the keypad, walk out, and if anyone comes in, it counts down 15 seconds for them to type the code. If they don't, a loud wailing siren goes off and doesn't stop until the code is typed.
Because the UNO R4 has Wi-Fi, it also runs its own phone app over your home Wi-Fi (arm it, turn it off, test the siren, see the history), and it sends alerts to your phone wherever you are: "Alarm armed", "Motion detected", "ALARM going off!", "Alarm stopped".

How it works
- The motion sensor (the white dome) sees the body heat of a person moving across the room. Its OUT pin goes HIGH when it sees movement.
- The keypad is 16 buttons in a grid of 4 rows and 4 columns. The program checks one row at a time to work out which key is pressed. You don't press Enter: as soon as the last keys you typed match the code, it reacts.
- The siren sweeps up and down between 1.8 and 3 kHz, where the little buzzer is loudest. The R4's pins can only give a tiny current (8 mA), so they switch a ULN2003 driver board, which gives the buzzer the full 5 volts.
- Wi-Fi: the board serves the app's web page itself, so no computer or server is needed. Alerts go through ntfy.sh, a free notification service: the board posts a message, and the ntfy app on your phone pops it up.
- It remembers whether it was armed, so unplugging it or pressing reset doesn't turn it off.
| State | What happens |
|---|---|
| OFF | Ignores movement. Type the code to arm it. |
| ARMING | Beeps every second for 15 s so you can leave. |
| ARMED | The L light blinks every 2 s. Movement starts the countdown. |
| COUNTDOWN | Warning beeps for 15 s (faster near the end). Type the code to stop it. |
| ALARM | Siren non-stop until the code is typed. |
Build it
1Upload the code first
Download the code below, open it in the Arduino app, pick Arduino UNO R4 WiFi, and upload. It's easier to check each part as you add it when the program is already running. New to this? See Getting started.
2Wire everything
Unplug the USB, then connect the three parts. No breadboard needed.

| Part | Its pin | Arduino pin |
|---|---|---|
| Motion sensor | GND | 4 |
| Motion sensor | OUT | 3 |
| Motion sensor | VCC | 2 |
| Keypad (keys facing up, holes left to right) | 8 holes | 12, 11, 10, 9, 8, 7, 6, 5 |
| Driver board | + | 5V |
| Driver board | − | GND |
| Driver board | 1N1 (IN1) | A0 |
| Driver board | 1N4 (IN4) | A3 |
| Buzzer | S and − | the two END holes of the driver's white socket, either way round |
Tip: The motion sensor uses so little power that pins 2 and 4 can power it. If you plug VCC and GND in the wrong way round, the program notices and swaps them for you.
Here's the driver board on a real build. The jumper next to the + and − pins stays on.

3Connect it to your Wi-Fi
The first time it starts, the alarm makes its own Wi-Fi network called Motion-Alarm-Setup.
- On your phone, join Motion-Alarm-Setup.
- Open http://192.168.4.1 in the phone's browser.
- Pick your home Wi-Fi and type its password. It has to be a 2.4 GHz network (most routers have one).
- The alarm joins your Wi-Fi within a few seconds. The password stays on the board and isn't sent anywhere else.
4Turn on phone alerts
- Install the free ntfy app.
- In the alarm's app, look under Phone alerts for its topic name (it starts with
alarm-). - In ntfy, tap +, type the topic name, and subscribe.
Warning: Anyone who knows the topic name can read your alerts, so keep it to yourself. Each alarm makes its own random topic the first time it starts.
5Open the app
The "Alarm is online" alert in ntfy has a link to the app. You can also type i in the Serial Monitor to see its address. On Android, the Motion Alarm app (download below) finds the alarm on your Wi-Fi by itself and opens it full screen.
Using it
- Arm: type the code on the keypad or in the app. You get 15 seconds to leave.
- Turn off: type the code. It works in every state.
- Change the code: while it's OFF, press
*, type the new code (4 to 8 keys), press#, type it again, press#. - Serial Monitor commands (9600 baud):
?status,iWi-Fi info,ttest siren,mmute,kshow which keys are pressed,dturn off,rreset the code to 1234,wWi-Fi setup mode.
The 3D-printed case
The case has a slanted front so the motion sensor looks out across the room, the keypad stuck on top, the driver board and buzzer at the back behind a grille, and holes for USB-C and power. Print the test pieces first to check your sensor fits the dome hole and the clips.

If it doesn't work
- It says "motion" all the time: the motion sensor's VCC and GND are probably swapped (the program swaps them once by itself, so check the wires if it keeps happening). Also, the sensor needs about a minute to settle after power-on.
- The alarm sets itself off a second after the siren stops: a loud sound stopping suddenly makes these sensors twitch. The program ignores the sensor for 5 seconds after any sound to stop this.
- The wrong keys show up: type
kin the Serial Monitor and press keys. If pressing 1 doesn't show 1, the keypad wires are in a different order. - It won't join the Wi-Fi: check the network is 2.4 GHz and retype the password carefully (capital letters count). Type
win the Serial Monitor to go back to setup. - The siren is quiet or silent: check the driver board's + and − are on 5V and GND, and the jumper next to them is on.
- The sensor's knobs: the one near the edge is sensitivity (range), the one toward the middle is how long OUT stays on. Turning the time knob down is fine.
The code
Three files that go in one folder: download the .zip, unzip it, and open MotionAlarmWiFi.ino. Pick Arduino UNO R4 WiFi as the board (see Getting started). The starting code is 1234; change it from the keypad once it's running.
// Motion Alarm (Wi-Fi) for the Arduino UNO R4 WiFi.// Same alarm as MotionAlarm (R3), plus a phone app served over the home Wi-Fi// and alerts through the free ntfy app.//// Wiring (same holes as the R3 version, except the buzzer):// Motion sensor (3 female-to-male wires): GND -> pin 4, OUT -> pin 3, VCC -> pin 2// (if VCC and GND are the other way round, the program notices and swaps them)// Keypad (8 male-to-male wires), its holes left to right with the keys facing up:// 12, 11, 10, 9, 8, 7, 6, 5// Buzzer: through the ULN2003 driver board from the starter kit, because R4// pins only give 8 mA (R3: 20 mA). Driver "+" -> 5V, "-" -> GND,// IN1 -> A0, IN4 -> A3. The buzzer's two pins go in the two END holes of the// driver's white socket, either way round: one end hole is the + supply and// the other is OUT1 or OUT4 (depending on the board), and A0 and A3 are// driven together, so either one works.// Light: the built-in "L" light (pin 13), and icons on the 12x8 LED matrix.//// How it works (same as the R3 version):// OFF -> type the code -> ARMING (15 s to leave) -> ARMED -> movement ->// COUNTDOWN (15 s to type the code) -> ALARM (siren until the code).// The code works on the keypad and in the phone app.// Change the code: while OFF, press *, new code, #, new code again, #.//// Wi-Fi:// The first time (or after pressing # five times while OFF), it makes its own// network "Motion-Alarm-Setup". Join it with the phone and open// http://192.168.4.1 to pick the home Wi-Fi and type its password. The// password stays on the board (EEPROM) - it never goes anywhere else.// Once on the home Wi-Fi, the app is at http://<its address>/, and ntfy gets// an "Alarm is online" message with the link. Phone alerts are sent to// ntfy.sh on a random topic made the first time it starts (shown in the app):// armed, turned off (and by what), motion detected (the moment the countdown// starts), and the siren going off (loudest). The motion alert goes out before// the 15 s countdown starts, so sending it doesn't eat into that time. Other// alerts wait until nobody is using the keypad (sending takes a second or two).//// Serial: "?" status, "i" Wi-Fi info, "t" test siren, "m" mute, "k" show keys,// "d" turn off, "r" reset the code to the default, "w" Wi-Fi setup mode.#include <WiFiS3.h>#include <EEPROM.h>#include "FspTimer.h"#include "Arduino_LED_Matrix.h"#include "web_pages.h"#include "icon.h"const int PIR_PIN = 3;int pirVcc = 2; // pins 2 and 4 trade places if the sensor'sint pirGnd = 4; // power wires turn out to be the other way roundconst int BUZ_A = A0; // to the driver's IN1const int BUZ_A2 = A3; // to the driver's IN4, driven the same as BUZ_Aconst int BUZ_B = -1; // opposite-phase output for a push-pull driver, or -1const int LIGHT = 13;const int ROW_PINS[4] = {12, 11, 10, 9};const int 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";const char SETUP_SSID[] = "Motion-Alarm-Setup";// 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 int EE_WIFI_MAGIC = 16;const int EE_SSID = 17; // 33 bytesconst int EE_PASS = 50; // 65 bytesconst int EE_TOPIC_MAGIC = 120;const int EE_TOPIC = 121; // 17 bytesconst int EE_FORCE_SETUP = 140;const byte MUTE_MAGIC = 0xA5;const byte CODE_MAGIC = 0x5C;const byte WIFI_MAGIC = 0x57;const byte TOPIC_MAGIC = 0x7A;const byte FORCE_MAGIC = 0x3C;enum State : byte { DISARMED = 0, ARMED = 1, ALARM = 2, ARMING = 3, ENTRY = 4 };State state = DISARMED;struct Note { bool used; byte prio; char title[32]; char body[80]; char tags[24]; }; // a waiting phone alertbool 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;byte hashCount = 0; // # presses in a row while OFF (5 = Wi-Fi setup)// ---------- History (shown in the app) ----------struct Event { unsigned long ms; char m[40]; };const int MAX_EVENTS = 10;Event events[MAX_EVENTS];int eventCount = 0;void addEvent(const char *m) { memmove(&events[1], &events[0], sizeof(Event) * (MAX_EVENTS - 1)); events[0].ms = millis(); strncpy(events[0].m, m, sizeof(events[0].m) - 1); events[0].m[sizeof(events[0].m) - 1] = 0; if (eventCount < MAX_EVENTS) eventCount++; Serial.print(F("Event: ")); Serial.println(m);}// ---------- LED matrix icons ----------ArduinoLEDMatrix matrix;const char *const ICON_OFF[8] = { "............", "....####....", "...#....#...", "...#....#...", "...#....#...", "...#....#...", "....####....", "............"};const char *const ICON_ARMED[8] = { "....####....", "...#....#...", "...#....#...", "..########..", "..########..", "..###..###..", "..########..", "..########.."};const char *const ICON_ARMING[8] = { "....####....", "...#....#...", "........#...", "..########..", "..########..", "..###..###..", "..########..", "..########.."};const char *const ICON_COUNT[8] = { "..########..", "...#....#...", "....#..#....", ".....##.....", ".....##.....", "....#..#....", "...#.##.#...", "..########.."};const char *const ICON_ALARM[8] = { ".....##.....", ".....##.....", ".....##.....", ".....##.....", ".....##.....", "............", ".....##.....", ".....##....."};const char *const ICON_WIFI[8] = { "............", "...######...", "..#......#..", ".#..####..#.", "...#....#...", ".....##.....", ".....##.....", "............"};const char *const ICON_BLANK[8] = { "............", "............", "............", "............", "............", "............", "............", "............"};void showIcon(const char *const rows[8]) { static const char *const *shown = nullptr; if (rows == shown) return; shown = rows; static uint8_t buf[8][12]; for (int r = 0; r < 8; r++) for (int c = 0; c < 12; c++) buf[r][c] = rows[r][c] == '#'; matrix.renderBitmap(buf, 8, 12);}// ---------- Sound: a timer flips the buzzer output ----------// The pitch is changed with set_period(), which takes effect at the next tick.// (set_frequency() closes and reopens the timer every time, which made the// sweep grind.) The wail itself runs inside the timer interrupt: every 2.5 ms of// sound it steps to the next pitch, so it stays smooth even while the main// program is busy for a second or two sending a phone alert.//// Each tick flips the output, and the timer's next period is set to the length// of the phase after the one starting now (the period register is buffered).// dutyPm is how much of each wave the buzzer is pushed (per thousand): less// than 500 pushes it more gently, which can stop a buzzer rattling.FspTimer sirenTimer;volatile bool buzLevel = false;bool sirenReady = false;float timerClock = 0; // timer counts per secondconst int WAIL_STEPS = 400; // pitch steps in one up-and-down sweepuint32_t wailFull[WAIL_STEPS]; // timer counts per whole wave at each stepuint32_t stepCounts = 1; // timer counts in one stepconst uint16_t DEFAULT_DUTY = 500;volatile uint16_t dutyPm = DEFAULT_DUTY;volatile bool wailing = false;volatile int wailIdx = 0;volatile uint32_t wailAcc = 0;volatile uint32_t curFull = 1; // counts per whole wave right nowvolatile uint32_t activeLen = 1; // length of the phase that is runninguint32_t phaseLen(uint32_t full, bool high) { uint32_t on = (uint32_t)((uint64_t)full * dutyPm / 1000); return high ? on : full - on;}void sirenTick(timer_callback_args_t *) { buzLevel = !buzLevel; digitalWrite(BUZ_A, buzLevel); digitalWrite(BUZ_A2, buzLevel); if (BUZ_B >= 0) digitalWrite(BUZ_B, !buzLevel); if (wailing) { wailAcc += activeLen; if (wailAcc >= stepCounts) { wailAcc -= stepCounts; if (++wailIdx >= WAIL_STEPS) wailIdx = 0; curFull = wailFull[wailIdx]; } } activeLen = phaseLen(curFull, buzLevel); // what the buffer loaded for this phase sirenTimer.set_period(phaseLen(curFull, !buzLevel));}uint32_t fullWaveCounts(float hz) { return (uint32_t)(timerClock / hz + 0.5f);}void sirenSetup() { uint8_t type = 0; int8_t ch = FspTimer::get_available_timer(type); if (ch < 0) { Serial.println(F("No free timer for the siren")); return; } sirenTimer.begin(TIMER_MODE_PERIODIC, type, ch, 4800.0f, 50.0f, sirenTick, nullptr); sirenTimer.setup_overflow_irq(4); // ahead of the LED matrix and Wi-Fi interrupts sirenTimer.open(); sirenTimer.stop(); sirenTimer.set_period_buffer(true); timerClock = (float)sirenTimer.get_period_raw() * 4800.0f; stepCounts = (uint32_t)(timerClock * WAIL_MS / WAIL_STEPS / 1000.0f); int half = WAIL_STEPS / 2; for (int i = 0; i < WAIL_STEPS; i++) { float f = i < half ? WAIL_LOW + (WAIL_HIGH - WAIL_LOW) * (float)i / half : WAIL_HIGH - (WAIL_HIGH - WAIL_LOW) * (float)(i - half) / half; wailFull[i] = fullWaveCounts(f); } sirenReady = true;}void startTimer() { if (!sounding) { sirenTimer.start(); sounding = true; }}// A steady note.void soundOn(unsigned int hz) { if (!sirenReady) return; noInterrupts(); wailing = false; curFull = fullWaveCounts(hz); activeLen = phaseLen(curFull, false); interrupts(); if (!sounding) sirenTimer.set_period(phaseLen(curFull, true)); startTimer();}// The up-and-down siren (runs by itself until soundOff).void wailOn() { if (!sirenReady || (wailing && sounding)) return; noInterrupts(); wailIdx = 0; wailAcc = 0; curFull = wailFull[0]; activeLen = phaseLen(curFull, false); wailing = true; interrupts(); if (!sounding) sirenTimer.set_period(phaseLen(curFull, true)); startTimer();}void soundOff() { if (sounding && sirenReady) sirenTimer.stop(); sounding = false; wailing = false; buzLevel = false; digitalWrite(BUZ_A, LOW); digitalWrite(BUZ_A2, LOW); if (BUZ_B >= 0) digitalWrite(BUZ_B, LOW); 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 should be on: keeps it wailing, flashes the light.void updateSiren(unsigned long now) { digitalWrite(LIGHT, ((now - stateStart) / 100) % 2 == 0); if (muted) { if (sounding) soundOff(); } else wailOn();}// ---------- Phone alerts (ntfy) ----------char topic[17];Note notes[6];char armedBy[10] = "keypad"; // who armed it, for the "armed" alertunsigned long lastNoteFail = 0;void queueNote(const char *title, const char *body, byte prio, const char *tags) { for (Note &n : notes) { if (n.used) continue; n.used = true; n.prio = prio; strncpy(n.title, title, sizeof(n.title) - 1); n.title[sizeof(n.title) - 1] = 0; strncpy(n.body, body, sizeof(n.body) - 1); n.body[sizeof(n.body) - 1] = 0; strncpy(n.tags, tags, sizeof(n.tags) - 1); n.tags[sizeof(n.tags) - 1] = 0; return; }}// ---------- Wi-Fi and the app ----------char wifiSsid[33];char wifiPass[65];bool haveWifi = false;bool setupMode = false;bool wifiUp = false;unsigned long lastWifiTry = 0;unsigned long lastWifiCheck = 0;WiFiServer server(80);char netNames[12][33];int netCount = 0;byte wrongTries = 0;unsigned long lockUntil = 0;const char *ipStr() { static char buf[16]; IPAddress ip = setupMode ? IPAddress(192, 168, 4, 1) : WiFi.localIP(); snprintf(buf, sizeof buf, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); return buf;}void readEepromString(int at, char *dst, int size) { for (int i = 0; i < size; i++) dst[i] = EEPROM.read(at + i); dst[size - 1] = 0;}void writeEepromString(int at, const char *src, int size) { for (int i = 0; i < size; i++) { char c = i < (int)strlen(src) ? src[i] : 0; EEPROM.update(at + i, c); }}void loadWifi() { haveWifi = EEPROM.read(EE_WIFI_MAGIC) == WIFI_MAGIC; if (haveWifi) { readEepromString(EE_SSID, wifiSsid, sizeof wifiSsid); readEepromString(EE_PASS, wifiPass, sizeof wifiPass); haveWifi = wifiSsid[0] != 0; }}void loadTopic() { if (EEPROM.read(EE_TOPIC_MAGIC) == TOPIC_MAGIC) { readEepromString(EE_TOPIC, topic, sizeof topic); if (strlen(topic) == 16) return; } // A random name nobody would guess, so only you get the alerts. unsigned long seed = micros(); for (int i = 0; i < 32; i++) seed = seed * 31 + analogRead(A3) + (analogRead(A4) << 4) + (analogRead(A5) << 8); randomSeed(seed); strcpy(topic, "alarm-"); const char *chars = "abcdefghijkmnpqrstuvwxyz23456789"; for (int i = 6; i < 16; i++) topic[i] = chars[random(32)]; topic[16] = 0; writeEepromString(EE_TOPIC, topic, sizeof topic); EEPROM.update(EE_TOPIC_MAGIC, TOPIC_MAGIC);}void startSetupMode() { setupMode = true; Serial.println(F("Wi-Fi setup: looking for networks...")); netCount = 0; int n = WiFi.scanNetworks(); for (int i = 0; i < n && netCount < 12; i++) { const char *s = WiFi.SSID(i); if (!s || !s[0]) continue; bool dup = false; for (int j = 0; j < netCount; j++) if (!strcmp(netNames[j], s)) dup = true; if (dup) continue; strncpy(netNames[netCount], s, 32); netNames[netCount][32] = 0; netCount++; } WiFi.config(IPAddress(192, 168, 4, 1)); if (WiFi.beginAP(SETUP_SSID) != WL_AP_LISTENING) { Serial.println(F("Couldn't start the setup network")); return; } delay(1000); server.begin(); Serial.print(F("Setup network \"")); Serial.print(SETUP_SSID); Serial.println(F("\" is on. Join it and open http://192.168.4.1")); addEvent("Wi-Fi setup mode");}bool connectWifi() { lastWifiTry = millis(); Serial.print(F("Joining Wi-Fi \"")); Serial.print(wifiSsid); Serial.println(F("\"...")); int st = WiFi.begin(wifiSsid, wifiPass); if (st != WL_CONNECTED) { Serial.print(F("Couldn't join the Wi-Fi (")); Serial.print(st == WL_NO_SSID_AVAIL ? F("network not found / too weak") : st == WL_CONNECT_FAILED ? F("refused - usually the password") : F("no answer")); Serial.print(F(", code ")); Serial.print(st); Serial.println(F(") - will keep trying every minute")); return false; } unsigned long t0 = millis(); while (WiFi.localIP() == IPAddress(0, 0, 0, 0) && millis() - t0 < 8000) delay(100); wifiUp = true; server.begin(); Serial.print(F("On Wi-Fi. App: http://")); Serial.println(ipStr()); char body[64]; snprintf(body, sizeof body, "Tap to open the app: http://%s/", ipStr()); queueNote("Alarm is online", body, 2, "house"); addEvent("Connected to Wi-Fi"); return true;}void startWifi() { if (WiFi.status() == WL_NO_MODULE) { Serial.println(F("Wi-Fi chip not answering - running without Wi-Fi")); return; } bool force = EEPROM.read(EE_FORCE_SETUP) == FORCE_MAGIC; if (force) EEPROM.update(EE_FORCE_SETUP, 0); if (force || !haveWifi) startSetupMode(); else connectWifi();}// Keep the Wi-Fi going without getting in the way of the alarm: never while// someone needs the keypad (arming or the countdown).void tendWifi(unsigned long now) { if (setupMode || !haveWifi) return; if (now - lastWifiCheck < 10000UL) return; lastWifiCheck = now; if (wifiUp && WiFi.status() != WL_CONNECTED) { wifiUp = false; Serial.println(F("Lost the Wi-Fi")); } if (!wifiUp && (state == DISARMED || state == ARMED || state == ALARM) && now - lastWifiTry >= 60000UL) connectWifi();}bool postNote(const Note &n, unsigned long waitMs = 5000, int connectMs = 0) { WiFiSSLClient cl; if (connectMs) cl.setConnectionTimeout(connectMs); if (!cl.connect("ntfy.sh", 443)) return false; cl.print("POST /"); cl.print(topic); cl.println(" HTTP/1.1"); cl.println("Host: ntfy.sh"); cl.print("Title: "); cl.println(n.title); cl.print("Priority: "); cl.println(n.prio); if (n.tags[0]) { cl.print("Tags: "); cl.println(n.tags); } cl.print("Click: http://"); cl.print(ipStr()); cl.println("/"); cl.print("Content-Length: "); cl.println(strlen(n.body)); cl.println("Connection: close"); cl.println(); cl.print(n.body); unsigned long t0 = millis(); char line[24]; int len = 0; while (millis() - t0 < waitMs && len < (int)sizeof(line) - 1) { if (!cl.available()) { delay(5); continue; } char c = cl.read(); if (c == '\n') break; line[len++] = c; } line[len] = 0; cl.stop(); return strstr(line, " 200") != nullptr;}// Motion seen while armed: tell the phone right away (with short time limits,// since the countdown is about to start). If it can't go now, it's queued.void sendMotionAlert() { Note m = {true, 4, "Motion detected", "Movement seen - 15 s to type the code before the siren.", "warning"}; if (wifiUp) { digitalWrite(LIGHT, HIGH); bool ok = postNote(m, 2000, 3000); digitalWrite(LIGHT, LOW); if (ok) { Serial.println(F("Alert sent: Motion detected")); return; } } queueNote(m.title, m.body, m.prio, m.tags);}// Sends waiting alerts. Each one takes a second or two, so not while someone// is using the keypad.void sendNotes(unsigned long now) { if (!wifiUp || state == ENTRY || state == ARMING) return; if (lastNoteFail && now - lastNoteFail < 30000UL) return; for (Note &n : notes) { if (!n.used) continue; if (postNote(n)) { n.used = false; lastNoteFail = 0; Serial.print(F("Alert sent: ")); Serial.println(n.title); } else { lastNoteFail = millis(); Serial.println(F("Couldn't send an alert - will try again")); } return; // one at a time, back to the alarm in between }}// ----- tiny web server -----bool readLine(WiFiClient &c, char *buf, int size, unsigned long timeout) { int n = 0; unsigned long t0 = millis(); while (millis() - t0 < timeout) { if (!c.available()) { if (!c.connected()) break; delay(1); continue; } char ch = c.read(); if (ch == '\r') continue; if (ch == '\n') { buf[n] = 0; return true; } if (n < size - 1) buf[n++] = ch; } buf[n] = 0; return n > 0;}void sendHead(WiFiClient &c, int status, const char *type, size_t len) { c.print("HTTP/1.1 "); c.print(status); c.println(status == 200 ? " OK" : status == 404 ? " Not Found" : " Bad Request"); c.print("Content-Type: "); c.println(type); c.print("Content-Length: "); c.println(len); c.println("Cache-Control: no-store"); c.println("Connection: close"); c.println();}void sendBytes(WiFiClient &c, const char *type, const uint8_t *p, size_t len) { sendHead(c, 200, type, len); while (len) { size_t k = len > 1024 ? 1024 : len; c.write(p, k); p += k; len -= k; }}void sendText(WiFiClient &c, const char *type, const char *s) { sendBytes(c, type, (const uint8_t *)s, strlen(s));}// Adds s to a JSON string being built, with quotes and escapes.void jsonStr(char *out, size_t size, const char *s) { size_t n = strlen(out); if (n + 2 >= size) return; out[n++] = '"'; for (; *s && n + 3 < size; s++) { if (*s == '"' || *s == '\\') out[n++] = '\\'; if ((unsigned char)*s < 0x20) continue; out[n++] = *s; } out[n++] = '"'; out[n] = 0;}void jsonAdd(char *out, size_t size, const char *s) { strncat(out, s, size - strlen(out) - 1);}// Value of key in a form body like "a=arm&c=1234", with %xx and + decoded.bool formValue(const char *body, const char *key, char *out, int size) { int kl = strlen(key); const char *p = body; while (p && *p) { if (!strncmp(p, key, kl) && p[kl] == '=') { p += kl + 1; int n = 0; while (*p && *p != '&' && n < size - 1) { if (*p == '+') { out[n++] = ' '; p++; } else if (*p == '%' && p[1] && p[2]) { char hex[3] = {p[1], p[2], 0}; out[n++] = (char)strtol(hex, nullptr, 16); p += 3; } else out[n++] = *p++; } out[n] = 0; return true; } p = strchr(p, '&'); if (p) p++; } out[0] = 0; return false;}const char *stateName() { switch (state) { case DISARMED: return "OFF"; case ARMING: return "ARMING"; case ARMED: return "ARMED"; case ENTRY: return "COUNTDOWN"; case ALARM: return "ALARM"; } return "?";}void sendStatus(WiFiClient &c) { static char j[1400]; unsigned long now = millis(); long left = 0; if (state == ARMING) left = ((long)EXIT_MS - (long)(now - stateStart) + 999) / 1000; if (state == ENTRY) left = ((long)ENTRY_MS - (long)(now - stateStart) + 999) / 1000; if (left < 0) left = 0; long locked = (lockUntil && (long)(lockUntil - now) > 0) ? (long)(lockUntil - now) / 1000 + 1 : 0; snprintf(j, sizeof j, "{\"state\":\"%s\",\"left\":%ld,\"muted\":%s,\"motion\":%s,\"alarms\":%lu,\"locked\":%ld,\"topic\":\"%s\",\"events\":[", stateName(), left, muted ? "true" : "false", digitalRead(PIR_PIN) ? "true" : "false", motionCount, locked, topic); for (int i = 0; i < eventCount; i++) { char item[24]; snprintf(item, sizeof item, "%s{\"ago\":%lu,\"m\":", i ? "," : "", (now - events[i].ms) / 1000); jsonAdd(j, sizeof j, item); jsonStr(j, sizeof j, events[i].m); jsonAdd(j, sizeof j, "}"); } jsonAdd(j, sizeof j, "]}"); sendText(c, "application/json", j);}void reply(WiFiClient &c, bool ok, const char *msg) { char j[120] = ""; jsonAdd(j, sizeof j, ok ? "{\"ok\":true,\"msg\":" : "{\"ok\":false,\"msg\":"); jsonStr(j, sizeof j, msg); jsonAdd(j, sizeof j, "}"); sendText(c, "application/json", j);}void handleCmd(WiFiClient &c, const char *body) { char a[12], given[12]; formValue(body, "a", a, sizeof a); formValue(body, "c", given, sizeof given); unsigned long now = millis(); if (lockUntil && (long)(lockUntil - now) > 0) { reply(c, false, "Too many wrong codes - wait a minute"); return; } if (strlen(given) != codeLen || memcmp(given, code, codeLen)) { if (++wrongTries >= 5) { wrongTries = 0; lockUntil = now + 60000UL; addEvent("Wrong codes on the phone"); } reply(c, false, "Wrong code"); return; } wrongTries = 0; if (!strcmp(a, "arm")) { if (state != DISARMED) { reply(c, false, "It's already on"); return; } reply(c, true, "Arming - 15 s to leave"); goodTone(); strcpy(armedBy, "phone"); setState(ARMING); addEvent("Armed from phone"); } else if (!strcmp(a, "disarm")) { if (state == DISARMED) { reply(c, false, "It's already off"); return; } reply(c, true, "Turned off"); turnOff("phone"); } else if (!strcmp(a, "test")) { if (state != DISARMED) { reply(c, false, "Turn it off first"); return; } reply(c, true, "Testing the siren"); stateStart = millis(); testUntil = millis() + 3000; addEvent("Siren test"); } else if (!strcmp(a, "mute")) { muted = !muted; EEPROM.update(EE_MUTE, muted ? MUTE_MAGIC : 0xFF); if (muted && sounding) soundOff(); reply(c, true, muted ? "Sound off (light only)" : "Sound on"); } else { reply(c, false, "Unknown command"); }}void sendSetupInfo(WiFiClient &c) { static char j[700]; snprintf(j, sizeof j, "{\"topic\":\"%s\",\"nets\":[", topic); for (int i = 0; i < netCount; i++) { if (i) jsonAdd(j, sizeof j, ","); jsonStr(j, sizeof j, netNames[i]); } jsonAdd(j, sizeof j, "]}"); sendText(c, "application/json", j);}void handleWifiForm(WiFiClient &c, const char *body) { char s[33], p[65]; formValue(body, "s", s, sizeof s); formValue(body, "p", p, sizeof p); if (!s[0]) { reply(c, false, "No Wi-Fi name"); return; } writeEepromString(EE_SSID, s, sizeof wifiSsid); writeEepromString(EE_PASS, p, sizeof wifiPass); EEPROM.update(EE_WIFI_MAGIC, WIFI_MAGIC); reply(c, true, "Saved"); c.flush(); delay(500); c.stop(); Serial.print(F("Wi-Fi saved: \"")); Serial.print(s); Serial.println(F("\". Restarting to join it.")); delay(1000); NVIC_SystemReset();}void handleWeb() { static unsigned long lastPoll = 0; if (!wifiUp && !setupMode) return; if (millis() - lastPoll < 20) return; lastPoll = millis(); WiFiClient c = server.available(); if (!c) return; char req[120], hdr[120]; if (!readLine(c, req, sizeof req, 500)) { c.stop(); return; } int contentLen = 0; while (readLine(c, hdr, sizeof hdr, 500) && hdr[0]) if (!strncasecmp(hdr, "Content-Length:", 15)) contentLen = atoi(hdr + 15); static char body[260]; int n = 0; unsigned long t0 = millis(); while (n < contentLen && n < (int)sizeof(body) - 1 && millis() - t0 < 500) { if (c.available()) body[n++] = c.read(); else delay(1); } body[n] = 0; bool post = !strncmp(req, "POST ", 5); char *path = strchr(req, ' '); if (!path) { c.stop(); return; } path++; char *end = strpbrk(path, " ?"); if (end) *end = 0; if (setupMode) { if (post && !strcmp(path, "/api/wifi")) handleWifiForm(c, body); else if (!strcmp(path, "/api/setupinfo")) sendSetupInfo(c); else sendText(c, "text/html; charset=utf-8", SETUP_HTML); // every other address shows the setup page } else { if (!strcmp(path, "/") || !strcmp(path, "/index.html")) sendText(c, "text/html; charset=utf-8", APP_HTML); else if (!strcmp(path, "/api/status")) sendStatus(c); else if (post && !strcmp(path, "/api/cmd")) handleCmd(c, body); else if (!strcmp(path, "/manifest.json")) sendText(c, "application/manifest+json", MANIFEST_JSON); else if (!strcmp(path, "/icon.png")) sendBytes(c, "image/png", ICON_PNG, sizeof ICON_PNG); else { sendHead(c, 404, "text/plain", 9); c.print("Not found"); } } c.flush(); c.stop();}// ---------- 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;}// Turns the alarm off, from the keypad or the phone.void turnOff(const char *by) { State was = state; bool wasAlarm = state == ALARM; if (wasAlarm) { soundOff(); digitalWrite(LIGHT, LOW); delay(300); } goodTone(); setState(DISARMED); char m[40]; snprintf(m, sizeof m, "Turned off by %s", by); addEvent(m); char body[80]; if (wasAlarm) { snprintf(body, sizeof body, "The siren was turned off by %s.", by); queueNote("Alarm stopped", body, 4, "white_check_mark"); } else if (was == ENTRY) { snprintf(body, sizeof body, "Movement was seen, then it was turned off by %s in time.", by); queueNote("Alarm turned off", body, 3, "unlock"); } else if (was == ARMED) { snprintf(body, sizeof body, "Turned off by %s.", by); queueNote("Alarm turned off", body, 3, "unlock"); } // cancelled while arming: it was never on, so no alert}// ---------- 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() { if (state == DISARMED) { goodTone(); strcpy(armedBy, "keypad"); setState(ARMING); addEvent("Armed by keypad"); } else { turnOff("keypad"); }}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")); addEvent("Code changed"); } 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 (state == DISARMED) { hashCount = k == '#' ? hashCount + 1 : 0; if (hashCount >= 5) { // # five times while OFF: Wi-Fi setup Serial.println(F("Restarting into Wi-Fi setup")); twoBeeps(); EEPROM.update(EE_FORCE_SETUP, FORCE_MAGIC); delay(300); NVIC_SystemReset(); } } 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 (int r = 0; r < 4; r++) { pinMode(ROW_PINS[r], OUTPUT); digitalWrite(ROW_PINS[r], LOW); delayMicroseconds(5); for (int 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; } int 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 printWifiInfo() { if (setupMode) { Serial.print(F("Wi-Fi: setup mode, network \"")); Serial.print(SETUP_SSID); Serial.println(F("\", page http://192.168.4.1")); } else if (!haveWifi) { Serial.println(F("Wi-Fi: not set up")); } else { Serial.print(F("Wi-Fi: \"")); Serial.print(wifiSsid); Serial.print(wifiUp ? F("\" connected, app http://") : F("\" not connected")); if (wifiUp) { Serial.print(ipStr()); Serial.print(F("/ signal ")); Serial.print(WiFi.RSSI()); Serial.print(F(" dBm")); } Serial.println(); } Serial.print(F("ntfy topic: ")); Serial.println(topic);}void printStatus() { Serial.print(F("State: ")); Serial.print(stateName()); 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(); printWifiInfo();}void handleSerial() { while (Serial.available()) { char c = Serial.read(); if (c == '?') { printStatus(); } else if (c == 'i') { printWifiInfo(); } else if (c == 'n' && !setupMode) { // Wi-Fi networks in range, with signal strength Serial.print(F("Wi-Fi chip firmware: ")); Serial.println(WiFi.firmwareVersion()); int n = WiFi.scanNetworks(); Serial.print(n); Serial.println(F(" networks:")); for (int i = 0; i < n; i++) { Serial.print(F(" ")); Serial.print(WiFi.RSSI(i)); Serial.print(F(" dBm ")); Serial.println(WiFi.SSID(i)); } } else if (c == 'V') { // the siren at 3 strengths, to compare by ear const uint16_t duties[3] = {500, 300, 150}; for (int k = 0; k < 3; k++) { dutyPm = duties[k]; Serial.print(F("Version ")); Serial.print(k + 1); Serial.print(F(": push ")); Serial.print(duties[k] / 10); Serial.println(F("%")); wailOn(); delay(3500); soundOff(); delay(1500); } dutyPm = DEFAULT_DUTY; } else if (c == 'T') { // longer test, with the program busy for 1.5 s Serial.println(F("Test siren, 6 s, busy for 1.5 s like when sending an alert")); stateStart = millis(); testUntil = millis() + 6000; updateSiren(millis()); delay(1500); } else if (c == 't') { Serial.println(F("Test siren")); stateStart = millis(); 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 (state != DISARMED) turnOff("computer"); } else if (c == 'r') { saveCode(DEFAULT_CODE, strlen(DEFAULT_CODE)); Serial.println(F("Code reset to the default")); } else if (c == 'w') { Serial.println(F("Restarting into Wi-Fi setup")); EEPROM.update(EE_FORCE_SETUP, FORCE_MAGIC); delay(300); NVIC_SystemReset(); } }}void updateMatrix(unsigned long now) { switch (state) { case DISARMED: showIcon(setupMode ? ICON_WIFI : ICON_OFF); break; case ARMING: showIcon((now / 500) % 2 ? ICON_ARMING : ICON_BLANK); break; case ARMED: showIcon(ICON_ARMED); break; case ENTRY: showIcon(ICON_COUNT); break; case ALARM: showIcon((now / 250) % 2 ? ICON_ALARM : ICON_BLANK); break; }}void setup() { Serial.begin(9600); matrix.begin(); powerSensor(); pinMode(PIR_PIN, INPUT_PULLUP); // reads HIGH if the OUT wire is missing, not random pinMode(LIGHT, OUTPUT); for (int i = 0; i < 4; i++) { pinMode(ROW_PINS[i], INPUT_PULLUP); pinMode(COL_PINS[i], INPUT_PULLUP); } pinMode(BUZ_A, OUTPUT); digitalWrite(BUZ_A, LOW); pinMode(BUZ_A2, OUTPUT); digitalWrite(BUZ_A2, LOW); if (BUZ_B >= 0) { pinMode(BUZ_B, OUTPUT); digitalWrite(BUZ_B, LOW); } sirenSetup(); muted = EEPROM.read(EE_MUTE) == MUTE_MAGIC; loadCode(); loadTopic(); loadWifi(); warmStart = millis(); Serial.println(F("Motion alarm (Wi-Fi) starting")); byte saved = EEPROM.read(EE_STATE); if (saved == ALARM) { setState(ALARM); // it was going off: keep going until the code addEvent("Still going off after a restart"); queueNote("ALARM going off!", "The alarm restarted while going off, and it's still going.", 5, "rotating_light"); } else if (saved == ENTRY) { setState(ENTRY); // unplugged during the countdown: start it again } else if (saved == ARMED) { strcpy(armedBy, "restart"); setState(ARMING); stateStart = millis() - EXIT_MS; // no walk-away time needed, just the warm-up twoBeeps(); } else { setState(DISARMED); twoBeeps(); } updateMatrix(millis()); startWifi();}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); char body[64]; if (!strcmp(armedBy, "restart")) snprintf(body, sizeof body, "Armed again after the alarm restarted."); else snprintf(body, sizeof body, "Armed by %s. Watching for movement.", armedBy); queueNote("Alarm armed", body, 3, "lock"); } 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); addEvent("Motion - countdown"); setState(ENTRY); sendMotionAlert(); stateStart = millis(); // the 15 s starts once the alert has gone } 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); addEvent("ALARM! Siren on"); queueNote("ALARM going off!", "Movement was seen and the code wasn't typed in time.", 5, "rotating_light"); } else if (now - lastBeep >= gap || t < 50) { lastBeep = now; tone2(2000, 70); } break; } case ALARM: updateSiren(now); break; } updateMatrix(now); handleWeb(); now = millis(); sendNotes(now); tendWifi(now);}
The phone app's screens (made from the HTML pages in the web folder).
// Made by tools/make_web.py from the files in web/ - edit those, not this.#pragma onceconst char APP_HTML[] = R"~~~(<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#0f172a"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><meta name="apple-mobile-web-app-title" content="Alarm"><link rel="manifest" href="/manifest.json"><link rel="apple-touch-icon" href="/icon.png"><link rel="icon" href="/icon.png"><title>Motion Alarm</title><style>:root{--bg:#0f172a;--card:#1e293b;--key:#334155;--text:#f1f5f9;--muted:#94a3b8;--off:#475569;--arming:#b45309;--armed:#1d4ed8;--count:#c2410c;--alarm:#dc2626;--ok:#15803d}*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}body{margin:0;background:var(--bg);color:var(--text);font:16px/1.4 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;padding:16px;padding-top:max(16px,env(safe-area-inset-top))}.wrap{max-width:440px;margin:0 auto}header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}h1{font-size:20px;margin:0}#conn{font-size:13px;color:var(--muted)}#conn i{display:inline-block;width:9px;height:9px;border-radius:50%;background:var(--ok);margin-right:6px}#conn.bad i{background:var(--alarm)}.status{border-radius:18px;padding:22px 16px;text-align:center;background:var(--off);transition:background .3s}.status .big{font-size:36px;font-weight:800;letter-spacing:1px}.status .sub{margin-top:4px;font-size:17px}.s-ARMING{background:var(--arming)}.s-ARMED{background:var(--armed)}.s-COUNTDOWN{background:var(--count)}.s-ALARM{background:var(--alarm);animation:flash 1s infinite}@keyframes flash{50%{background:#7f1d1d}}.card{background:var(--card);border-radius:16px;padding:16px;margin-top:14px}.dots{display:flex;justify-content:center;gap:12px;height:20px;margin-bottom:14px}.dots span{width:15px;height:15px;border-radius:50%;border:2px solid var(--muted)}.dots span.f{background:var(--text);border-color:var(--text)}.pad{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}button{font:inherit;border:0;border-radius:14px;color:var(--text);cursor:pointer}.pad button{background:var(--key);font-size:26px;font-weight:600;padding:13px 0}.pad button:active{filter:brightness(1.3)}.act{width:100%;margin-top:12px;padding:16px;font-size:19px;font-weight:700;background:var(--armed)}.act.off{background:var(--ok)}.act:disabled{opacity:.4}#msg{min-height:24px;text-align:center;margin-top:10px;font-weight:600}.row{display:flex;gap:10px}.row button{flex:1;padding:13px;font-size:15px;font-weight:600;background:var(--key)}.info{display:flex;justify-content:space-between;color:var(--muted);font-size:14px;margin-top:12px}h2{font-size:13px;margin:0 0 6px;color:var(--muted);font-weight:700;text-transform:uppercase;letter-spacing:.8px}ul{list-style:none;margin:0;padding:0}li{display:flex;justify-content:space-between;padding:8px 0;border-top:1px solid var(--key);font-size:15px}li:first-child{border-top:0}li time{color:var(--muted);font-size:13px;white-space:nowrap;margin-left:10px}.topic{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:17px;background:var(--bg);padding:8px 12px;border-radius:8px;margin:8px 0;user-select:all;word-break:break-all}.small{font-size:14px;color:var(--muted)}a{color:#93c5fd}</style></head><body><div class="wrap"><header><h1>Motion Alarm</h1><div id="conn"><i></i><span>Connecting…</span></div></header><div id="st" class="status"><div class="big" id="big">…</div><div class="sub" id="sub"> </div></div><div class="card"> <div class="dots" id="dots"></div> <div class="pad" id="pad"></div> <button class="act" id="act" disabled>…</button> <div id="msg"></div></div><div class="card"> <div class="row"><button id="test">Test siren</button><button id="mute">Sound: on</button></div> <div class="info"><span id="sensor">Sensor: –</span><span id="count"></span></div></div><div class="card"><h2>History</h2><ul id="ev"><li>Nothing yet</li></ul></div><div class="card"><h2>Phone alerts</h2> <div class="small">In the free <b>ntfy</b> app, tap <b>+</b> and subscribe to:</div> <div class="topic" id="topic">…</div> <div class="small">You'll get an alert whenever the siren goes off, wherever you are. <a id="sub" href="#">Open in ntfy</a></div></div></div><script>const $=id=>document.getElementById(id);let code='',S=null,busy=false;const NAMES={OFF:['OFF','Movement is ignored'],ARMING:['ARMING','Leave the room now'],ARMED:['ARMED','Watching for movement'],COUNTDOWN:['MOTION!','Type the code to stop it'],ALARM:['ALARM!','The siren is going off']};['1','2','3','4','5','6','7','8','9','⌫','0','OK'].forEach(k=>{const b=document.createElement('button');b.textContent=k;b.onclick=()=>press(k);$('pad').appendChild(b)});function say(t,good){$('msg').textContent=t;$('msg').style.color=good?'#86efac':'#fca5a5'}function press(k){if(k==='OK'){go();return}if(k==='⌫')code=code.slice(0,-1);else if(code.length<8)code+=k;say('');dots()}function dots(){const d=$('dots');d.innerHTML='';for(let i=0;i<Math.max(4,code.length);i++){const s=document.createElement('span');if(i<code.length)s.className='f';d.appendChild(s)}upd()}function upd(){const a=$('act');if(!S){a.disabled=true;return}const off=S.state==='OFF';a.textContent=off?'Arm':S.state==='ARMING'?'Cancel':'Turn off';a.className='act'+(off?'':' off');a.disabled=busy||code.length<4}async function cmd(a){busy=true;upd();try{const r=await fetch('/api/cmd',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams({a:a,c:code})});const j=await r.json();say(j.msg,j.ok);return j.ok}catch(e){say('Could not reach the alarm');return false}finally{busy=false;poll()}}async function go(){if(!S||code.length<4)return;if(await cmd(S.state==='OFF'?'arm':'disarm')){code='';dots()}}function needCode(a){if(code.length<4){say('Type your code first');return}cmd(a)}$('act').onclick=go;$('test').onclick=()=>needCode('test');$('mute').onclick=()=>needCode('mute');function when(s){if(s<60)return'just now';const d=new Date(Date.now()-s*1000),t=d.toLocaleTimeString([],{hour:'numeric',minute:'2-digit'});return(Date.now()-d<864e5&&d.getDate()===new Date().getDate())?t:d.toLocaleDateString([],{weekday:'short'})+' '+t}function render(){const n=NAMES[S.state]||[S.state,''];$('st').className='status s-'+S.state;$('big').textContent=n[0];let sub=n[1];if(S.left>0)sub+=' · '+S.left+' s';if(S.locked>0)sub='Too many wrong codes – wait '+S.locked+' s';$('sub').textContent=sub;$('mute').textContent='Sound: '+(S.muted?'off':'on');$('sensor').textContent='Sensor: '+(S.motion?'sees movement':'quiet');$('count').textContent=S.alarms+(S.alarms==1?' alarm':' alarms')+' since start';$('topic').textContent=S.topic;$('sub').href='ntfy://ntfy.sh/'+S.topic;const ul=$('ev');ul.innerHTML='';if(!S.events.length)ul.innerHTML='<li>Nothing yet</li>';S.events.forEach(e=>{const li=document.createElement('li'),a=document.createElement('span'),b=document.createElement('time');a.textContent=e.m;b.textContent=when(e.ago);li.append(a,b);ul.appendChild(li)});upd()}async function poll(){try{const r=await fetch('/api/status',{cache:'no-store'});S=await r.json();render();$('conn').className='';$('conn').lastChild.textContent='Connected'}catch(e){$('conn').className='bad';$('conn').lastChild.textContent='Not connected'}}dots();poll();setInterval(poll,1500);</script></body></html>)~~~";const char SETUP_HTML[] = R"~~~(<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Alarm Wi-Fi setup</title><style>body{margin:0;background:#0f172a;color:#f1f5f9;font:16px/1.45 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;padding:16px}.wrap{max-width:440px;margin:0 auto}h1{font-size:22px}.card{background:#1e293b;border-radius:16px;padding:16px;margin-top:14px}label{display:block;margin:12px 0 6px;color:#94a3b8;font-size:14px}select,input{width:100%;box-sizing:border-box;font:inherit;padding:12px;border-radius:10px;border:1px solid #475569;background:#0f172a;color:#f1f5f9}button{width:100%;margin-top:16px;padding:15px;font:inherit;font-weight:700;font-size:18px;border:0;border-radius:14px;background:#1d4ed8;color:#fff}.topic{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:17px;background:#0f172a;padding:8px 12px;border-radius:8px;margin:8px 0;user-select:all;word-break:break-all}.small{font-size:14px;color:#94a3b8}#m{margin-top:14px;font-weight:600;color:#86efac}</style></head><body><div class="wrap"><h1>Connect the alarm to your Wi-Fi</h1><div class="card"><b>Step 1: phone alerts</b> <div class="small">In the free <b>ntfy</b> app, tap <b>+</b> and subscribe to this topic. Once the alarm is on your Wi-Fi, ntfy will send you the link to its app.</div> <div class="topic" id="t">…</div></div><form class="card" id="f"><b>Step 2: your Wi-Fi</b> <label for="s">Wi-Fi name</label> <select id="s"><option value="">Looking…</option></select> <label for="s2">Not in the list? Type it here</label> <input id="s2" autocomplete="off" autocapitalize="none"> <label for="p">Wi-Fi password</label> <input id="p" type="password" autocomplete="off"> <label><input type="checkbox" id="show" style="width:auto"> Show password</label> <button>Connect</button> <div id="m"></div></form></div><script>const $=id=>document.getElementById(id);$('show').onchange=()=>{$('p').type=$('show').checked?'text':'password'};fetch('/api/setupinfo').then(r=>r.json()).then(j=>{$('t').textContent=j.topic;const s=$('s');s.innerHTML='';j.nets.forEach(n=>{const o=document.createElement('option');o.value=o.textContent=n;s.appendChild(o)});if(!j.nets.length)s.innerHTML='<option value="">None found - type it below</option>'});$('f').onsubmit=async e=>{e.preventDefault();const ssid=$('s2').value.trim()||$('s').value;if(!ssid){$('m').textContent='Pick or type your Wi-Fi name';return}try{await fetch('/api/wifi',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams({s:ssid,p:$('p').value})})}catch(e){}$('m').textContent='Saved! The alarm is joining your Wi-Fi now. Switch your phone back to your normal Wi-Fi, then watch for the ntfy alert with the app link.'};</script></body></html>)~~~";const char MANIFEST_JSON[] = R"~~~({"name":"Motion Alarm","short_name":"Alarm","start_url":"/","display":"standalone","background_color":"#0f172a","theme_color":"#0f172a","icons":[{"src":"/icon.png","sizes":"192x192","type":"image/png","purpose":"any"}]})~~~";
The app's icon, stored as numbers so the board can send it.
// Made by tools/make_web.py - the app's home-screen icon (PNG).#pragma onceconst uint8_t ICON_PNG[2025] = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0, 0x08, 0x06, 0x00, 0x00, 0x00, 0x52, 0xdc, 0x6c, 0x07, 0x00, 0x00, 0x07, 0xb0, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0xed, 0xdd, 0x31, 0x8b, 0x24, 0x45, 0x14, 0xc0, 0xf1, 0xf9, 0x14, 0x06, 0xb2, 0x5c, 0x60, 0x20, 0x68, 0x20, 0x28, 0x78, 0x8c, 0x18, 0x88, 0xa1, 0x20, 0x22, 0xc2, 0x18, 0x18, 0x8a, 0x22, 0x08, 0x06, 0x8b, 0x99, 0x06, 0x06, 0xc6, 0x8b, 0x81, 0x81, 0x60, 0x6c, 0x34, 0x91, 0x86, 0xa6, 0x06, 0xab, 0x91, 0xb9, 0x89, 0xc8, 0xfa, 0x09, 0x0c, 0x0c, 0x2e, 0x5a, 0xb7, 0xce, 0xe9, 0x63, 0xd8, 0x9b, 0xd9, 0x9d, 0xee, 0xae, 0xee, 0xae, 0xaa, 0xf7, 0x1b, 0xf8, 0x83, 0x82, 0xce, 0xcd, 0x54, 0xbf, 0x7f, 0xd5, 0xab, 0xf7, 0xaa, 0xe6, 0x56, 0xab, 0x85, 0x5e, 0x0f, 0x5e, 0x78, 0x7d, 0x7d, 0xc3, 0xe6, 0x86, 0x8b, 0x1b, 0xb6, 0x37, 0x5c, 0xde, 0x70, 0x8d, 0x10, 0x6c, 0x77, 0x9c, 0xef, 0x62, 0xe0, 0x6c, 0xd5, 0xfa, 0x2b, 0x7d, 0xc9, 0xdd, 0x17, 0xde, 0x0a, 0x00, 0x1c, 0xe0, 0x6a, 0x17, 0x1b, 0x9b, 0xd6, 0x02, 0xff, 0xdc, 0xec, 0x8e, 0x81, 0xab, 0xc4, 0xba, 0xe6, 0xd9, 0xfe, 0xc2, 0x43, 0x44, 0xa6, 0x95, 0xe1, 0x5c, 0xe0, 0x83, 0x08, 0x25, 0x8b, 0xb0, 0x4b, 0x75, 0x3c, 0x28, 0xcc, 0x21, 0xc2, 0xba, 0xb4, 0x59, 0x5f, 0x8e, 0x8f, 0xb9, 0xb9, 0x30, 0xeb, 0xc3, 0x6a, 0xb0, 0xd4, 0x6a, 0xa0, 0x9c, 0x89, 0x82, 0xd8, 0x48, 0x79, 0x20, 0x25, 0x9a, 0x29, 0xf8, 0xaf, 0x0c, 0x36, 0x4a, 0xed, 0x1d, 0x08, 0x7e, 0x90, 0x40, 0xf0, 0x83, 0x04, 0x79, 0x05, 0x90, 0xf3, 0xa3, 0x36, 0xce, 0x55, 0x7b, 0xa0, 0x3a, 0xa4, 0xce, 0x8f, 0xe0, 0x9c, 0x8d, 0xc9, 0xfb, 0x0d, 0x20, 0x6a, 0xe7, 0x52, 0xde, 0x0f, 0xfb, 0x01, 0xa9, 0x0f, 0xa4, 0x42, 0x52, 0x1f, 0x48, 0x85, 0xee, 0x15, 0xc0, 0x79, 0x7e, 0xb4, 0xca, 0xda, 0xec, 0x0f, 0xab, 0x80, 0xd9, 0x1f, 0x56, 0x81, 0xc3, 0x02, 0x18, 0x20, 0xc4, 0x3c, 0x26, 0xa1, 0xf2, 0x83, 0xd0, 0x15, 0x21, 0x75, 0x7f, 0x84, 0xed, 0x0b, 0xd8, 0xfc, 0x22, 0xf4, 0x66, 0x58, 0xfa, 0x83, 0xd0, 0x69, 0x90, 0x13, 0x9f, 0x08, 0x7d, 0x52, 0xd4, 0x60, 0x20, 0x6c, 0x35, 0xe8, 0xc1, 0xff, 0xbf, 0xd2, 0x6c, 0x40, 0x10, 0x8d, 0xab, 0x4e, 0x80, 0x8d, 0xc1, 0x40, 0x44, 0x74, 0x7f, 0xa1, 0x2b, 0x6c, 0x03, 0x8c, 0xd0, 0x1b, 0x61, 0x0d, 0x30, 0x84, 0x6e, 0x88, 0x19, 0x04, 0x84, 0xae, 0x04, 0x19, 0x04, 0x10, 0x00, 0x20, 0x00, 0x40, 0x00, 0x80, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x04, 0x00, 0x08, 0x80, 0x3c, 0x7c, 0xf2, 0xe6, 0x4b, 0x77, 0x62, 0x8c, 0x08, 0x50, 0x25, 0x0f, 0x5f, 0x7e, 0xf8, 0x24, 0x88, 0x7f, 0xfc, 0xe0, 0xb9, 0xc7, 0xfc, 0xf6, 0xe1, 0x83, 0xeb, 0x47, 0x9f, 0x3f, 0x33, 0x8a, 0xf4, 0x1e, 0xdd, 0xfb, 0x75, 0xef, 0x9f, 0xfe, 0x2c, 0x63, 0x4e, 0x80, 0xc5, 0x83, 0x3d, 0x57, 0x90, 0x8f, 0x95, 0x83, 0x14, 0x04, 0x98, 0x2d, 0xe0, 0xff, 0xfa, 0xf4, 0xd9, 0xc5, 0x02, 0xfe, 0x3e, 0xd2, 0x67, 0x23, 0x04, 0x01, 0xb2, 0xf0, 0xce, 0x6b, 0xaf, 0x5c, 0x7f, 0xf7, 0xde, 0xf3, 0x8b, 0xce, 0xf0, 0x39, 0x56, 0x88, 0xf4, 0x1d, 0xd2, 0x77, 0xf1, 0x4c, 0x09, 0x70, 0xd2, 0x4c, 0x9f, 0x02, 0xa6, 0xe4, 0x59, 0x7e, 0xcc, 0xea, 0x90, 0xbe, 0x9b, 0x95, 0x81, 0x00, 0x4f, 0xf1, 0xd5, 0x5b, 0x2f, 0x56, 0x3d, 0xd3, 0x0f, 0x59, 0x19, 0xd2, 0x77, 0xf6, 0xec, 0x03, 0x0b, 0xd0, 0xf2, 0x6c, 0x6f, 0x55, 0x20, 0xc0, 0xbd, 0xb9, 0x7d, 0xd4, 0xa0, 0x3f, 0x46, 0xd0, 0xbd, 0x42, 0x1c, 0x01, 0xd2, 0xc3, 0x8d, 0x94, 0xe6, 0x8c, 0x49, 0x8f, 0x02, 0x89, 0xd0, 0xbe, 0x00, 0x69, 0x79, 0x4f, 0xa5, 0x41, 0xc1, 0xdd, 0x8f, 0x34, 0x66, 0x01, 0x52, 0xa3, 0x76, 0x05, 0xe8, 0x72, 0xfc, 0x52, 0x82, 0xe9, 0x36, 0xe9, 0xb3, 0x75, 0x9d, 0xdd, 0xf4, 0xcf, 0x87, 0xfe, 0x9b, 0x52, 0x52, 0xa3, 0x86, 0x45, 0x68, 0x53, 0x80, 0x54, 0xe1, 0x98, 0x7b, 0x73, 0xbb, 0xdf, 0x95, 0xcd, 0xdd, 0x88, 0xba, 0x7d, 0xcc, 0x62, 0xee, 0x54, 0x2e, 0x8d, 0x65, 0xa3, 0x55, 0xa3, 0xb6, 0x04, 0x48, 0x81, 0x32, 0x57, 0x70, 0xa4, 0x40, 0x4c, 0x41, 0xb1, 0x64, 0xbe, 0x9c, 0xfe, 0xec, 0xf4, 0x19, 0xe6, 0x5a, 0x2d, 0xd2, 0xd8, 0x36, 0xb6, 0x1a, 0xb4, 0x23, 0x40, 0x0a, 0x84, 0xe8, 0x5d, 0xd5, 0xb9, 0xba, 0xd7, 0x0d, 0xad, 0x06, 0xf5, 0x0b, 0x30, 0xe5, 0xac, 0xdf, 0x2d, 0xfd, 0x35, 0xce, 0x7a, 0xe9, 0x33, 0x4f, 0x99, 0x0a, 0x36, 0xb2, 0x1a, 0xd4, 0x2d, 0x40, 0xca, 0x89, 0xd5, 0xc4, 0x97, 0xed, 0x7d, 0x54, 0x7e, 0x87, 0xa1, 0x5e, 0x01, 0x72, 0xe7, 0xbd, 0x0d, 0x6f, 0xf4, 0x26, 0x2f, 0x10, 0xa4, 0x67, 0x41, 0x80, 0x19, 0x97, 0xf6, 0x9c, 0x0f, 0x30, 0xea, 0xb9, 0x98, 0xdc, 0xe7, 0x9f, 0xd2, 0x33, 0xa9, 0x30, 0x25, 0xaa, 0x4b, 0x80, 0xb4, 0xdc, 0xe6, 0x0a, 0xfe, 0xf4, 0x3e, 0x53, 0x2d, 0xdf, 0xb7, 0x6f, 0x86, 0x0d, 0x59, 0xad, 0xf6, 0xff, 0xdf, 0x29, 0xaf, 0x4b, 0xd6, 0x32, 0xa6, 0xe1, 0x05, 0xc8, 0x95, 0xef, 0xe7, 0x4e, 0x75, 0xf6, 0x4b, 0x91, 0x73, 0xf4, 0x1e, 0xba, 0x4b, 0x2f, 0xb9, 0x4b, 0xb0, 0x39, 0x53, 0xa3, 0x8a, 0x24, 0xa8, 0x43, 0x80, 0x5c, 0x1b, 0xb8, 0xf4, 0x3e, 0x39, 0x6f, 0x87, 0x95, 0x74, 0x6c, 0x21, 0x57, 0xf3, 0xad, 0xa4, 0xb1, 0x26, 0x40, 0xa6, 0xcd, 0x6e, 0x8e, 0x92, 0x5d, 0x2d, 0x77, 0x06, 0x72, 0xec, 0x69, 0x72, 0x95, 0x96, 0x2b, 0xd8, 0x1c, 0x97, 0x2d, 0x40, 0x8e, 0xe0, 0x1f, 0x13, 0x0c, 0x29, 0xc5, 0xa8, 0xf9, 0x20, 0x5d, 0xfa, 0xec, 0x63, 0xd2, 0xa4, 0x1c, 0xcd, 0xc5, 0xc2, 0x25, 0x28, 0x57, 0x80, 0xb1, 0x81, 0x37, 0x66, 0xd6, 0x6f, 0xed, 0x86, 0xd8, 0x98, 0x55, 0x21, 0xc7, 0x6a, 0x50, 0xb0, 0x04, 0x65, 0x0a, 0x30, 0x36, 0xf8, 0x87, 0xe6, 0x9f, 0x4b, 0x1c, 0xa2, 0xab, 0xe5, 0x50, 0xdb, 0xd8, 0xbd, 0x41, 0xa1, 0x12, 0x94, 0x27, 0xc0, 0xd8, 0xe0, 0x1f, 0x52, 0x81, 0x68, 0x3d, 0xf0, 0x73, 0x89, 0x30, 0xb6, 0x12, 0x57, 0xa0, 0x04, 0x65, 0x09, 0x30, 0x26, 0xf8, 0x87, 0x34, 0x62, 0xa2, 0xdf, 0x12, 0x1b, 0x72, 0xfb, 0x6b, 0x6c, 0x23, 0xb2, 0x30, 0x09, 0xca, 0x12, 0x60, 0xe8, 0x0c, 0xd3, 0xf7, 0xf6, 0x52, 0x49, 0x97, 0x65, 0x6a, 0xbc, 0xf4, 0x32, 0xe6, 0x96, 0x5d, 0x61, 0x3d, 0x82, 0xf2, 0x52, 0xa0, 0xbe, 0x33, 0x4c, 0xdf, 0x19, 0x25, 0x67, 0xe7, 0x73, 0xcc, 0xef, 0x7c, 0x1e, 0x63, 0xa9, 0x15, 0x69, 0x48, 0x17, 0xb7, 0x8f, 0x04, 0x85, 0x1e, 0x95, 0x28, 0x73, 0x13, 0x7c, 0xaa, 0x04, 0x7d, 0x82, 0x7f, 0xae, 0xbb, 0xc1, 0xfb, 0x9d, 0xda, 0xb1, 0xcd, 0xa9, 0xae, 0xe9, 0x36, 0x67, 0xa7, 0xb9, 0xef, 0x6a, 0x7a, 0xca, 0x98, 0x16, 0x7c, 0x4e, 0xa8, 0xdc, 0x32, 0xe8, 0x7d, 0x12, 0xf4, 0xd9, 0xc4, 0xa5, 0x3c, 0x77, 0xca, 0xe0, 0x99, 0xf3, 0xf7, 0x38, 0xe7, 0xe8, 0x44, 0xa7, 0xb1, 0xea, 0xb3, 0x37, 0xb8, 0xab, 0x5f, 0x50, 0xf8, 0x21, 0xb9, 0x3a, 0x3b, 0xc1, 0x7d, 0x96, 0xea, 0xa9, 0x6e, 0x8a, 0x95, 0x72, 0x67, 0x60, 0xca, 0xb3, 0xfe, 0x7d, 0x26, 0x99, 0x43, 0xfb, 0x37, 0x9d, 0xe0, 0x09, 0x24, 0xe8, 0x53, 0xe3, 0xcf, 0x3d, 0x4b, 0xa6, 0xfc, 0xbc, 0xe4, 0x83, 0x5e, 0xe9, 0xb3, 0xe5, 0xde, 0x43, 0xf4, 0x09, 0xe2, 0x7d, 0x11, 0x2b, 0xb9, 0x23, 0x50, 0xcf, 0x69, 0xd0, 0x6e, 0x93, 0xb8, 0xc4, 0x35, 0xc9, 0xda, 0x6e, 0x88, 0xe5, 0x5e, 0x15, 0xfa, 0x74, 0xd5, 0xfb, 0x3c, 0x27, 0x02, 0x14, 0x7e, 0x61, 0xa6, 0xf6, 0xdf, 0xc4, 0xc9, 0x59, 0xee, 0xad, 0xf4, 0xc2, 0x4b, 0xbc, 0x9f, 0x45, 0xc9, 0x11, 0xfc, 0xad, 0xfd, 0xfc, 0x47, 0xae, 0x15, 0xb1, 0x41, 0x09, 0xda, 0x11, 0x20, 0x47, 0xf0, 0x57, 0x78, 0xa3, 0x69, 0xf6, 0x1e, 0x48, 0x63, 0x12, 0xb4, 0x21, 0x40, 0x8e, 0xe0, 0xaf, 0xe4, 0x02, 0x47, 0x11, 0x17, 0x8c, 0x1a, 0x92, 0xa0, 0x8d, 0xdf, 0x05, 0x1a, 0x13, 0xfc, 0xad, 0xcf, 0xfa, 0x53, 0xad, 0x06, 0x8d, 0x48, 0x10, 0x5b, 0x80, 0x20, 0xbf, 0x80, 0x3c, 0x49, 0x77, 0x9c, 0x00, 0x95, 0x1f, 0xa3, 0x8e, 0x94, 0xf2, 0xe4, 0x4e, 0x89, 0x2a, 0xfe, 0x1d, 0xa0, 0xb6, 0xab, 0x40, 0xa7, 0x4a, 0xb0, 0x44, 0xca, 0xf3, 0xd1, 0x67, 0x5f, 0x3c, 0x61, 0xfb, 0xd3, 0xcf, 0x07, 0xd9, 0xff, 0x6f, 0x4a, 0x3d, 0x89, 0xdb, 0x50, 0xf0, 0xb7, 0xd9, 0x07, 0xb8, 0x4f, 0x82, 0x39, 0x82, 0xff, 0xed, 0xf7, 0x3f, 0xbe, 0xfe, 0xf2, 0xeb, 0x6f, 0x1e, 0x07, 0xf5, 0x1f, 0x7f, 0xfe, 0x7d, 0xfd, 0xcf, 0xbf, 0x8f, 0x06, 0x91, 0xfe, 0xdf, 0xf4, 0x1e, 0xe9, 0xbd, 0xd2, 0x7b, 0x2e, 0x2d, 0x41, 0x63, 0xc1, 0xdf, 0xa6, 0x00, 0xc7, 0x1e, 0x64, 0xdf, 0x03, 0x5e, 0x43, 0x82, 0x7e, 0x6c, 0xc0, 0x9f, 0x2a, 0xc4, 0x94, 0x32, 0x1c, 0x3b, 0x38, 0xd8, 0x68, 0xa1, 0xa0, 0xdd, 0xbf, 0x21, 0x66, 0x5f, 0x82, 0xa9, 0x36, 0x6c, 0xaf, 0xbe, 0xf1, 0xee, 0xf5, 0xb7, 0xdf, 0xff, 0x30, 0x69, 0xd0, 0xdf, 0x25, 0x43, 0xfa, 0xb3, 0xd3, 0x67, 0x98, 0xba, 0xb0, 0xd0, 0x70, 0x95, 0xac, 0xed, 0xbf, 0x23, 0xac, 0x93, 0x20, 0xf7, 0x03, 0xec, 0x02, 0x7f, 0xee, 0xa0, 0x3f, 0xc6, 0x14, 0x22, 0x4c, 0x35, 0x76, 0x04, 0xa8, 0x98, 0xd2, 0x02, 0x7f, 0x0e, 0x11, 0x1a, 0x87, 0x00, 0xa7, 0x92, 0x36, 0xa2, 0x4b, 0xa4, 0x3a, 0x43, 0x52, 0xa3, 0xf4, 0x59, 0x3d, 0x33, 0x02, 0x64, 0x9b, 0xf5, 0x7f, 0xf9, 0xf5, 0xf7, 0xe2, 0x03, 0xff, 0x36, 0xe9, 0x33, 0x5b, 0x0d, 0x08, 0x30, 0xba, 0x76, 0x5f, 0xc3, 0xac, 0x7f, 0xd7, 0x6a, 0xb0, 0x44, 0x4f, 0x81, 0x00, 0x0d, 0x50, 0x72, 0xae, 0x3f, 0x64, 0x6f, 0xe0, 0x99, 0x12, 0xe0, 0x64, 0x52, 0xad, 0xbd, 0x95, 0xe0, 0xef, 0x48, 0xdf, 0xc9, 0xb3, 0x25, 0x40, 0xc8, 0xe0, 0x27, 0x01, 0x01, 0xc2, 0x07, 0x3f, 0x09, 0x08, 0x10, 0x3e, 0xf8, 0x49, 0x40, 0x80, 0xf0, 0xc1, 0x4f, 0x02, 0x02, 0x3c, 0xd5, 0xe0, 0x8a, 0x16, 0xfc, 0x1d, 0x1a, 0x66, 0xc1, 0x05, 0x48, 0xa7, 0x2a, 0xa3, 0x06, 0x7f, 0xc7, 0x1c, 0xc7, 0xac, 0x09, 0x50, 0x68, 0x87, 0xb7, 0xe6, 0x26, 0x57, 0xce, 0x66, 0x59, 0xe0, 0x8e, 0x71, 0x5c, 0x01, 0x22, 0xe6, 0xfd, 0xf6, 0x03, 0x04, 0x90, 0xfa, 0x48, 0x85, 0x08, 0x20, 0xf5, 0x39, 0x9c, 0x0a, 0x11, 0x40, 0xd5, 0x27, 0x34, 0x01, 0xab, 0x42, 0xf1, 0x04, 0x30, 0xfb, 0x5b, 0x05, 0xc2, 0x0a, 0x60, 0xf6, 0xb7, 0x0a, 0x84, 0x16, 0xc0, 0xec, 0x6f, 0x15, 0x08, 0x2b, 0x40, 0xba, 0x18, 0x22, 0xc0, 0x4f, 0x23, 0xd0, 0x25, 0x9a, 0x38, 0x02, 0xd4, 0x78, 0xad, 0x71, 0xc9, 0xeb, 0x94, 0x04, 0x68, 0xac, 0xeb, 0x2b, 0xb0, 0xfb, 0x11, 0xa4, 0x3b, 0x1c, 0x43, 0x00, 0x9b, 0x5f, 0x9b, 0xe1, 0xd0, 0x02, 0x38, 0xf6, 0xe0, 0x78, 0x44, 0x68, 0x01, 0x04, 0xf4, 0x30, 0x08, 0xa0, 0xfa, 0xa3, 0x1a, 0x44, 0x00, 0xf9, 0xbf, 0x7d, 0x00, 0x01, 0xe4, 0xff, 0xf6, 0x01, 0x04, 0xd0, 0xfd, 0xd5, 0x15, 0x26, 0x80, 0x0d, 0xb0, 0x8d, 0x30, 0x01, 0x08, 0x40, 0x00, 0x02, 0xb8, 0xf9, 0xe5, 0xa6, 0x18, 0x01, 0x94, 0x40, 0x95, 0x42, 0x09, 0x40, 0x00, 0x02, 0x10, 0x60, 0xda, 0xc3, 0x6c, 0xfb, 0x7f, 0x87, 0xee, 0xa9, 0xb4, 0xf4, 0x13, 0xe7, 0x4b, 0xfe, 0xb4, 0xfa, 0x90, 0xb1, 0xaf, 0xe4, 0x30, 0x5d, 0x1d, 0x02, 0x98, 0xc9, 0xad, 0x1c, 0x04, 0x10, 0x54, 0x04, 0x20, 0x00, 0x08, 0x40, 0x00, 0x10, 0x80, 0x00, 0x20, 0x00, 0x01, 0x40, 0x00, 0x02, 0x80, 0x00, 0x04, 0x00, 0x01, 0x08, 0x00, 0x02, 0x10, 0x00, 0x04, 0x20, 0x00, 0x08, 0xe0, 0x2c, 0x10, 0x16, 0xc4, 0x59, 0x20, 0x80, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x04, 0x00, 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0x40, 0x00, 0x80, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x04, 0x00, 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0x40, 0x00, 0x80, 0x00, 0x40, 0x7e, 0x01, 0xb6, 0x06, 0x02, 0x41, 0xd9, 0x10, 0x00, 0xe1, 0x05, 0x38, 0x37, 0x10, 0x08, 0xca, 0x59, 0x12, 0x60, 0x63, 0x20, 0x10, 0x91, 0x55, 0x7a, 0x25, 0x0b, 0x0c, 0x06, 0x02, 0x72, 0xb9, 0xea, 0x5e, 0x37, 0xff, 0x72, 0x65, 0x40, 0x10, 0x8c, 0x8b, 0x7d, 0x01, 0x6c, 0x84, 0x11, 0x6f, 0x03, 0xbc, 0x27, 0x80, 0x7d, 0x00, 0xe2, 0xe5, 0xff, 0xfb, 0x2f, 0x83, 0x82, 0x50, 0x0d, 0xb0, 0x03, 0x02, 0x48, 0x83, 0x10, 0x2f, 0xfd, 0xd9, 0x13, 0x60, 0x6d, 0x60, 0x10, 0x80, 0xab, 0xd5, 0xb1, 0x97, 0x6a, 0x10, 0x42, 0x55, 0x7f, 0x0e, 0x08, 0xa0, 0x2b, 0x8c, 0xf6, 0xbb, 0xbf, 0x77, 0xbd, 0xac, 0x02, 0x08, 0x39, 0xfb, 0x5b, 0x05, 0x10, 0x7e, 0xf6, 0xb7, 0x0a, 0x20, 0xf4, 0xec, 0xaf, 0x22, 0x84, 0x70, 0x95, 0x9f, 0x3b, 0x24, 0xb8, 0x30, 0x70, 0x68, 0x84, 0xf5, 0x6a, 0xc8, 0x4b, 0x2a, 0x84, 0x50, 0xa9, 0x8f, 0x54, 0x08, 0xe1, 0x53, 0x9f, 0x03, 0x12, 0x38, 0x28, 0x87, 0x2a, 0x83, 0xff, 0xe4, 0xaa, 0x8f, 0xfd, 0x00, 0xe4, 0xfd, 0xf7, 0x4b, 0xe0, 0xb0, 0x1c, 0xea, 0x3d, 0xec, 0x46, 0x02, 0x08, 0x7e, 0x12, 0x40, 0xf0, 0x67, 0x91, 0xc0, 0x71, 0x09, 0x94, 0xb6, 0xe1, 0x5d, 0xaf, 0xe6, 0x7c, 0xa9, 0x0e, 0xa1, 0xb9, 0x6a, 0xcf, 0x00, 0x09, 0xd2, 0x4f, 0xaa, 0x5c, 0x7a, 0x08, 0xa8, 0xae, 0xc9, 0x25, 0x25, 0x82, 0x94, 0xc7, 0x6a, 0x00, 0xb3, 0x7e, 0x76, 0x11, 0xd6, 0x44, 0xc0, 0x14, 0x81, 0xbf, 0x58, 0xae, 0x3f, 0x42, 0x04, 0x25, 0x53, 0x8c, 0x4d, 0x75, 0xea, 0x0a, 0xfc, 0x23, 0xa9, 0xd1, 0xb9, 0x55, 0x01, 0x3d, 0xd8, 0xce, 0x56, 0xd3, 0x5f, 0x40, 0x86, 0xcd, 0xee, 0x0b, 0x3a, 0x6a, 0x8d, 0x8e, 0xcb, 0xdd, 0x4c, 0xdf, 0x5e, 0xd0, 0x9f, 0x98, 0x2a, 0x6d, 0x76, 0xab, 0xc4, 0x16, 0x21, 0xd8, 0xec, 0x58, 0x3c, 0xb5, 0xf9, 0x0f, 0x1a, 0x11, 0x88, 0x0b, 0xfe, 0x19, 0xc8, 0x94, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,};

