Parts list
| Part | Qty | Approx. | Buy |
|---|---|---|---|
| Arduino Uno R3 (CH340 clone) | 1 | ₹450 | Buy Arduino Uno R3 (CH340 clone) |
| Rolling-shutter magnetic contact (heavy-duty reed switch + magnet) | 1 | ₹250 | Buy Rolling-shutter magnetic contact (heavy-duty reed switch + magnet) |
| PIR motion sensor HC-SR501 | 1 | ₹80 | Buy PIR motion sensor HC-SR501 |
| SIM800L GSM module + 2G-capable SIM | 1 | ₹350 | Buy SIM800L GSM module + 2G-capable SIM |
| LM2596 buck module set to 4.0 V + 1000 µF capacitor | 1 | ₹100 | Buy LM2596 buck module set to 4.0 V + 1000 µF capacitor |
| 1-channel relay module, 5 V | 1 | ₹60 | Buy 1-channel relay module, 5 V |
| 12 V piezo siren (110 dB) | 1 | ₹250 | Buy 12 V piezo siren (110 dB) |
| Active buzzer module | 1 | ₹25 | Buy Active buzzer module |
| Push button (hidden) | 1 | ₹20 | Buy Push button (hidden) |
| Resistors 10 kΩ + 20 kΩ (RX divider) | 1 | ₹5 | Buy Resistors 10 kΩ + 20 kΩ (RX divider) |
| 12 V 7 Ah battery + 12 V charger/SMPS (alarm power through cuts) | 1 | ₹1,400 | Buy 12 V 7 Ah battery + 12 V charger/SMPS (alarm power through cuts) |
| Total | ₹2,990 |
Affiliate disclosure: “Buy” links may earn FlashReady a small commission at no extra cost to you. Prices are indicative.
Wiring
| Arduino Uno R3 | Part · pin | Wire | Notes |
|---|---|---|---|
| D4 | Reed switch / MC-38 magnetic door sensor · WIRE 1 | blue | Shutter switch, INPUT_PULLUP — closed while the shutter is down |
| GND | Reed switch / MC-38 magnetic door sensor · WIRE 2 | black | |
| 5V | HC-SR501 PIR motion sensor · VCC | red | |
| D5 | HC-SR501 PIR motion sensor · OUT | yellow | |
| GND | HC-SR501 PIR motion sensor · GND | black | |
| D6 | Push button (6 mm tactile switch) · 1 | purple | Hidden arm / disarm button |
| GND | Push button (6 mm tactile switch) · 2 | black | |
| D7 | 1-channel relay module (5 V coil) · IN | orange | Active-LOW; HIGH before it becomes an output |
| 5V | 1-channel relay module (5 V coil) · DC+ | red | |
| GND | 1-channel relay module (5 V coil) · DC− | black | |
| 12 V battery (on its charger) + 12 V siren · +12V | 1-channel relay module (5 V coil) · COM | red | 12 V + to COM |
| 12 V battery (on its charger) + 12 V siren · Siren + | 1-channel relay module (5 V coil) · NO | red | NO to the siren +; siren − to the battery − |
| D8 | Active buzzer module (high-level trigger) · I/O | orange | Exit / entry beeps inside |
| 5V | Active buzzer module (high-level trigger) · VCC | red | |
| GND | Active buzzer module (high-level trigger) · GND | black | |
| D2 | SIM800L GSM/GPRS module (2G) · TXD | blue | |
| D3 | SIM800L GSM/GPRS module (2G) · RXD | yellow | Via 10 kΩ / 20 kΩ divider — Protects the SIM800L’s 2.8 V serial input |
| VIN | 12 V battery (on its charger) + 12 V siren · +12V | red | The Uno runs from the 12 V battery (VIN = the barrel-jack rail, 7–12 V) |
| GND | 12 V battery (on its charger) + 12 V siren · − | black | Battery − to Uno GND; the buck’s input − goes here too |
| LM2596 buck set to 4.0 V · + | SIM800L GSM/GPRS module (2G) · VCC | red | 4.0 V / 2 A buck fed from the 12 V supply; 1000 µF across VCC/GND at the module |
| LM2596 buck set to 4.0 V · - | SIM800L GSM/GPRS module (2G) · GND | black | |
| GND | SIM800L GSM/GPRS module (2G) · GND | black | Common ground: Uno GND and buck − both to SIM800L GND |
Board quirks: Power everything from a 12 V battery kept charged by a 12 V supply: the Uno through its barrel jack (VIN), the SIM800L through the 4.0 V buck, the siren directly. A thief who cuts the shop’s power — or an ordinary power cut — must not silence the alarm. Fix the reed switch to the floor or frame and the magnet to the shutter’s bottom slat, about 1 cm apart when it’s down. Floor-mount shutter contacts are made for this and survive being stepped on. Hide the arm/disarm button somewhere only you know (behind the counter, inside a cupboard). After pressing it to arm, you have 60 s to step out and pull the shutter down; when you open up, you have 30 s to reach it. SIM800L is a 2G module. Check that your operator still runs 2G in your area — many Indian circles still do for basic phones, but this is changing; a 4G module (A7670 family) uses similar AT commands.
Code
// Shop shutter burglar alarm with SMS and call — Arduino Uno + reed switch + PIR + SIM800L
// Shutter reed switch D4 to GND (closed = shutter down). PIR HC-SR501 OUT on D5.
// Hidden arm/disarm push button D6 to GND. Relay module IN on D7 (active-LOW) switches a 12 V siren.
// Buzzer module on D8 for the exit/entry beeps. SIM800L: TXD -> D2, RXD <- D3 through a divider.
#include <SoftwareSerial.h>
#include "fr_settings.h"
const char* ALERT_TO = "+91XXXXXXXXXX";
// Filled in from the FlashReady flasher over USB (kept in EEPROM), or type the value above.
FrSetting SETTINGS[] = {{"alert_to", &ALERT_TO, true}};
constexpr uint8_t SHUTTER_PIN = 4;
constexpr uint8_t PIR_PIN = 5;
constexpr uint8_t BUTTON_PIN = 6;
constexpr uint8_t RELAY_PIN = 7;
constexpr uint8_t BUZZER_PIN = 8;
constexpr bool RELAY_ACTIVE_LOW = true;
constexpr unsigned long EXIT_MS = 60000UL; // time to step out and pull the shutter down
constexpr unsigned long ENTRY_MS = 30000UL; // time to reach the hidden button after opening
constexpr unsigned long SIREN_MS = 180000UL; // siren limit per alarm (3 minutes)
constexpr uint8_t MAX_ALARMS = 3; // per arming: no endless SMS if a sensor fails
enum State { DISARMED, EXIT_DELAY, ARMED, ENTRY_DELAY, ALARM };
const char* const NAMES[] = {"DISARMED", "EXIT", "ARMED", "ENTRY", "ALARM"};
State state = DISARMED;
unsigned long since = 0;
uint8_t alarms = 0;
SoftwareSerial sim(2, 3); // RX <- SIM800L TXD, TX -> divider -> SIM800L RXD
bool shutterOpen() { return digitalRead(SHUTTER_PIN) == HIGH; } // magnet away = switch open
bool motion() { return digitalRead(PIR_PIN) == HIGH; }
void siren(bool on) { digitalWrite(RELAY_PIN, (on != RELAY_ACTIVE_LOW) ? HIGH : LOW); }
void enter(State s) {
state = s;
since = millis();
siren(s == ALARM);
digitalWrite(BUZZER_PIN, LOW);
Serial.print(F("-> "));
Serial.println(NAMES[s]);
}
void smsAndCall(const __FlashStringHelper* why) {
if (!*ALERT_TO) {
Serial.println(F("SMS off — no phone number set."));
return;
}
sim.println(F("AT+CMGF=1"));
delay(300);
sim.print(F("AT+CMGS=\""));
sim.print(ALERT_TO);
sim.println('"');
delay(500);
sim.print(F("SHOP ALARM: "));
sim.print(why);
sim.write(26);
delay(5000);
sim.print(F("ATD")); // ring the phone for 20 s so it isn't missed at night
sim.print(ALERT_TO);
sim.println(';');
delay(20000);
sim.println(F("ATH"));
}
void trigger(const __FlashStringHelper* why) {
if (alarms >= MAX_ALARMS) return;
alarms++;
enter(ALARM);
Serial.print(F("ALARM: "));
Serial.println(why);
smsAndCall(why); // the siren is already sounding while this runs
}
void setup() {
siren(false); // relay off before the pin becomes an output
pinMode(RELAY_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(SHUTTER_PIN, INPUT_PULLUP);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(PIR_PIN, INPUT);
Serial.begin(115200);
frSettingsBegin(SETTINGS);
if (!*ALERT_TO) Serial.println(F("SMS off — no phone number set. Flash from flashready.in or type it into the code."));
sim.begin(9600);
enter(DISARMED);
}
void loop() {
unsigned long now = millis();
unsigned long t = now - since;
// Hidden button: arm from DISARMED, disarm from anything else.
if (digitalRead(BUTTON_PIN) == LOW) {
delay(30);
if (digitalRead(BUTTON_PIN) == LOW) {
if (state == DISARMED) {
alarms = 0;
enter(EXIT_DELAY);
} else {
enter(DISARMED);
}
while (digitalRead(BUTTON_PIN) == LOW) delay(10);
return;
}
}
switch (state) {
case EXIT_DELAY:
digitalWrite(BUZZER_PIN, (t % 1000) < 100); // beep once a second
if (t > EXIT_MS) {
if (shutterOpen()) {
Serial.println(F("Shutter still open - not armed"));
digitalWrite(BUZZER_PIN, HIGH);
delay(1500);
enter(DISARMED);
} else {
enter(ARMED);
}
}
break;
case ARMED:
if (shutterOpen()) enter(ENTRY_DELAY);
else if (motion()) trigger(F("motion inside with the shutter down"));
break;
case ENTRY_DELAY:
digitalWrite(BUZZER_PIN, (t % 500) < 100); // fast beeps: disarm now
if (t > ENTRY_MS) trigger(F("shutter opened"));
break;
case ALARM:
if (t > SIREN_MS) {
siren(false); // silent after 3 minutes, but stays alarmed until the shutter is down
if (!shutterOpen()) enter(ARMED);
}
break;
default:
break;
}
static unsigned long lastLog = 0;
if (now - lastLog > 2000) {
lastLog = now;
Serial.print(F("state="));
Serial.print(NAMES[state]);
Serial.print(F(" shutter="));
Serial.print(shutterOpen() ? F("open") : F("closed"));
Serial.print(F(" pir="));
Serial.println(motion());
}
}// Shop shutter burglar alarm with SMS and call — Arduino Uno + reed switch + PIR + SIM800L
// Shutter reed switch D4 to GND (closed = shutter down). PIR HC-SR501 OUT on D5.
// Hidden arm/disarm push button D6 to GND. Relay module IN on D7 (active-LOW) switches a 12 V siren.
// Buzzer module on D8 for the exit/entry beeps. SIM800L: TXD -> D2, RXD <- D3 through a divider.
#include <SoftwareSerial.h>
#include "fr_settings.h"
const char* ALERT_TO = "+91XXXXXXXXXX";
// Filled in from the FlashReady flasher over USB (kept in EEPROM), or type the value above.
FrSetting SETTINGS[] = {{"alert_to", &ALERT_TO, true}};
constexpr uint8_t SHUTTER_PIN = 4;
constexpr uint8_t PIR_PIN = 5;
constexpr uint8_t BUTTON_PIN = 6;
constexpr uint8_t RELAY_PIN = 7;
constexpr uint8_t BUZZER_PIN = 8;
constexpr bool RELAY_ACTIVE_LOW = true;
constexpr unsigned long EXIT_MS = 60000UL; // time to step out and pull the shutter down
constexpr unsigned long ENTRY_MS = 30000UL; // time to reach the hidden button after opening
constexpr unsigned long SIREN_MS = 180000UL; // siren limit per alarm (3 minutes)
constexpr uint8_t MAX_ALARMS = 3; // per arming: no endless SMS if a sensor fails
enum State { DISARMED, EXIT_DELAY, ARMED, ENTRY_DELAY, ALARM };
const char* const NAMES[] = {"DISARMED", "EXIT", "ARMED", "ENTRY", "ALARM"};
State state = DISARMED;
unsigned long since = 0;
uint8_t alarms = 0;
SoftwareSerial sim(2, 3); // RX <- SIM800L TXD, TX -> divider -> SIM800L RXD
bool shutterOpen() { return digitalRead(SHUTTER_PIN) == HIGH; } // magnet away = switch open
bool motion() { return digitalRead(PIR_PIN) == HIGH; }
void siren(bool on) { digitalWrite(RELAY_PIN, (on != RELAY_ACTIVE_LOW) ? HIGH : LOW); }
void enter(State s) {
state = s;
since = millis();
siren(s == ALARM);
digitalWrite(BUZZER_PIN, LOW);
Serial.print(F("-> "));
Serial.println(NAMES[s]);
}
void smsAndCall(const __FlashStringHelper* why) {
if (!*ALERT_TO) {
Serial.println(F("SMS off — no phone number set."));
return;
}
sim.println(F("AT+CMGF=1"));
delay(300);
sim.print(F("AT+CMGS=\""));
sim.print(ALERT_TO);
sim.println('"');
delay(500);
sim.print(F("SHOP ALARM: "));
sim.print(why);
sim.write(26);
delay(5000);
sim.print(F("ATD")); // ring the phone for 20 s so it isn't missed at night
sim.print(ALERT_TO);
sim.println(';');
delay(20000);
sim.println(F("ATH"));
}
void trigger(const __FlashStringHelper* why) {
if (alarms >= MAX_ALARMS) return;
alarms++;
enter(ALARM);
Serial.print(F("ALARM: "));
Serial.println(why);
smsAndCall(why); // the siren is already sounding while this runs
}
void setup() {
siren(false); // relay off before the pin becomes an output
pinMode(RELAY_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(SHUTTER_PIN, INPUT_PULLUP);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(PIR_PIN, INPUT);
Serial.begin(115200);
frSettingsBegin(SETTINGS);
if (!*ALERT_TO) Serial.println(F("SMS off — no phone number set. Flash from flashready.in or type it into the code."));
sim.begin(9600);
enter(DISARMED);
}
void loop() {
unsigned long now = millis();
unsigned long t = now - since;
// Hidden button: arm from DISARMED, disarm from anything else.
if (digitalRead(BUTTON_PIN) == LOW) {
delay(30);
if (digitalRead(BUTTON_PIN) == LOW) {
if (state == DISARMED) {
alarms = 0;
enter(EXIT_DELAY);
} else {
enter(DISARMED);
}
while (digitalRead(BUTTON_PIN) == LOW) delay(10);
return;
}
}
switch (state) {
case EXIT_DELAY:
digitalWrite(BUZZER_PIN, (t % 1000) < 100); // beep once a second
if (t > EXIT_MS) {
if (shutterOpen()) {
Serial.println(F("Shutter still open - not armed"));
digitalWrite(BUZZER_PIN, HIGH);
delay(1500);
enter(DISARMED);
} else {
enter(ARMED);
}
}
break;
case ARMED:
if (shutterOpen()) enter(ENTRY_DELAY);
else if (motion()) trigger(F("motion inside with the shutter down"));
break;
case ENTRY_DELAY:
digitalWrite(BUZZER_PIN, (t % 500) < 100); // fast beeps: disarm now
if (t > ENTRY_MS) trigger(F("shutter opened"));
break;
case ALARM:
if (t > SIREN_MS) {
siren(false); // silent after 3 minutes, but stays alarmed until the shutter is down
if (!shutterOpen()) enter(ARMED);
}
break;
default:
break;
}
static unsigned long lastLog = 0;
if (now - lastLog > 2000) {
lastLog = now;
Serial.print(F("state="));
Serial.print(NAMES[state]);
Serial.print(F(" shutter="));
Serial.print(shutterOpen() ? F("open") : F("closed"));
Serial.print(F(" pir="));
Serial.println(motion());
}
}Arduino IDE: keep fr_settings.h in the same folder as the .ino (tab menu → New Tab, same name). Flashing from this page doesn’t need it.
// fr_settings.h — FlashReady on-board settings (Wi-Fi name, phone number, ...) over USB.
//
// Why: the pre-built file on flashready.in is the same for everyone, so your own settings
// can't be inside it. Instead the board asks for them over USB serial and keeps them in
// its EEPROM. If you use Arduino IDE you can still type them into the constants in the
// sketch — this file then changes nothing except a 2-second pause at start-up.
//
// The protocol, in plain words (one text line each way, ending in \n, at 115200 baud):
// 1. Right after Serial.begin(115200) the board prints
// FR?SETTINGS v1 keys=wifi_ssid,wifi_pass missing=wifi_pass
// "missing" lists the required settings that still have no real value.
// 2. If nothing is missing it listens for 2 seconds and then carries on. If something is
// missing it keeps listening, printing the line again every 2 seconds, for up to
// 30 seconds; then it carries on anyway and the sketch skips whatever needs the
// missing value (an alarm still sounds, it just can't send the SMS).
// 3. The flasher answers FR!SET wifi_ssid=My%20Home&wifi_pass=secret
// (values URL-encoded). The board stores them and answers FR!OK, then carries on with
// the new values. Errors: FR!ERR unknown <key>, FR!ERR long <key> (nothing is stored).
// A key that is left out keeps its stored value; "key=" (empty) removes it.
// "FR!SET" with nothing after it means "carry on now": FR!OK, nothing is stored.
// 4. FR!CLEAR wipes everything that was stored, answers FR!OK and keeps listening.
// Other lines are ignored.
//
// A stored value wins over the constant in the sketch. A constant that is still a
// placeholder ("YOUR_SSID", "your-wifi", "+91XXXXXXXXXX") or empty counts as not set.
//
// EEPROM block at FR_SETTINGS_ADDR: 'F' 'R' 0x01, length (uint16, low byte first), then
// "key=value\n" lines. Uses FR_SETTINGS_SIZE bytes; move FR_SETTINGS_ADDR with a #define
// before the #include if your sketch uses EEPROM for something else.
//
// Use in a sketch:
// const char* WIFI_SSID = "YOUR_SSID";
// FrSetting SETTINGS[] = { {"wifi_ssid", &WIFI_SSID, true} }; // key, constant, required
// void setup() { Serial.begin(115200); frSettingsBegin(SETTINGS); ... WiFi.begin(WIFI_SSID, ...); }
// After frSettingsBegin() each constant points at the value to use ("" if none is set).
#pragma once
#include <Arduino.h>
#include <EEPROM.h>
#include <string.h>
#ifndef FR_SETTINGS_ADDR
#define FR_SETTINGS_ADDR 0
#endif
#ifndef FR_SETTINGS_SIZE
#if defined(__AVR__)
#define FR_SETTINGS_SIZE 96 // 2 KB of RAM on an Uno: a phone number fits easily
#else
#define FR_SETTINGS_SIZE 512
#endif
#endif
#ifndef FR_LINE_MAX
#if defined(__AVR__)
#define FR_LINE_MAX 128
#else
#define FR_LINE_MAX 1024
#endif
#endif
#define FR_MAX_KEYS 8
#define FR_WAIT_MAX_MS 30000UL // never wait longer than this for missing settings
#define FR_DATA_MAX (FR_SETTINGS_SIZE - 5)
struct FrSetting {
const char* key; // [a-z0-9_], as in settings.json
const char** value; // the sketch's constant; replaced by the value to use
bool required;
};
// Stored settings in RAM as "key=value\0key=value\0" (only non-empty values).
static char frData[FR_DATA_MAX + 1];
static uint16_t frLen = 0;
static inline void frEepromBegin() {
#if defined(ESP32) || defined(ESP8266)
static bool started = false;
if (!started) EEPROM.begin(FR_SETTINGS_ADDR + FR_SETTINGS_SIZE);
started = true;
#endif
}
static inline void frLoad() {
frEepromBegin();
frLen = 0;
frData[0] = 0;
if (EEPROM.read(FR_SETTINGS_ADDR) != 'F' || EEPROM.read(FR_SETTINGS_ADDR + 1) != 'R' || EEPROM.read(FR_SETTINGS_ADDR + 2) != 1) return;
uint16_t n = EEPROM.read(FR_SETTINGS_ADDR + 3) | (EEPROM.read(FR_SETTINGS_ADDR + 4) << 8);
if (n > FR_DATA_MAX) return;
for (uint16_t i = 0; i < n; i++) {
char c = EEPROM.read(FR_SETTINGS_ADDR + 5 + i);
frData[i] = c == '\n' ? '\0' : c;
}
frData[n] = 0;
frLen = n;
}
static inline void frPut(int addr, uint8_t b) {
#if defined(__AVR__)
EEPROM.update(addr, b);
#else
EEPROM.write(addr, b);
#endif
}
static inline void frSave() {
frEepromBegin();
frPut(FR_SETTINGS_ADDR, 'F');
frPut(FR_SETTINGS_ADDR + 1, 'R');
frPut(FR_SETTINGS_ADDR + 2, 1);
frPut(FR_SETTINGS_ADDR + 3, frLen & 0xFF);
frPut(FR_SETTINGS_ADDR + 4, frLen >> 8);
for (uint16_t i = 0; i < frLen; i++) frPut(FR_SETTINGS_ADDR + 5 + i, frData[i] ? frData[i] : '\n');
#if defined(ESP32) || defined(ESP8266)
EEPROM.commit();
#endif
}
// The stored value for key, or nullptr.
static inline const char* frStored(const char* key) {
size_t kl = strlen(key);
for (uint16_t i = 0; i < frLen; i += strlen(frData + i) + 1) {
if (strncmp(frData + i, key, kl) == 0 && frData[i + kl] == '=') return frData + i + kl + 1;
}
return nullptr;
}
// Empty, "your_…" / "YOUR-…" / "your …", or a "+91XXXX…" phone number.
static inline bool frIsPlaceholder(const char* v) {
if (!v || !*v) return true;
if (strncasecmp(v, "your", 4) == 0 && (v[4] == '_' || v[4] == '-' || v[4] == ' ')) return true;
return v[0] == '+' && strstr(v, "XXXX") != nullptr;
}
// The stored value for key, else the fallback constant; "" if that is a placeholder.
static inline const char* frSettingsGet(const char* key, const char* fallback) {
const char* v = frStored(key);
if (!v) v = fallback;
return frIsPlaceholder(v) ? "" : v;
}
static inline bool frIsMissing(const FrSetting& s) {
return s.required && !*frSettingsGet(s.key, *s.value);
}
static inline void frRequest(FrSetting* s, uint8_t n) {
Serial.print(F("FR?SETTINGS v1 keys="));
for (uint8_t i = 0; i < n; i++) {
if (i) Serial.print(',');
Serial.print(s[i].key);
}
Serial.print(F(" missing="));
bool first = true;
for (uint8_t i = 0; i < n; i++) {
if (!frIsMissing(s[i])) continue;
if (!first) Serial.print(',');
Serial.print(s[i].key);
first = false;
}
Serial.print('\n');
}
static inline void frReply(const __FlashStringHelper* what, const char* key) {
Serial.print(what);
Serial.print(key);
Serial.print('\n');
}
static inline int8_t frHex(char c) {
if (c >= '0' && c <= '9') return c - '0';
c |= 0x20;
return (c >= 'a' && c <= 'f') ? c - 'a' + 10 : -1;
}
// Percent-decodes in place. Drops NUL, CR and LF: they can't be stored.
static inline void frDecode(char* s) {
char* o = s;
for (; *s; s++) {
char c = *s;
if (c == '%' && frHex(s[1]) >= 0 && frHex(s[2]) >= 0) {
c = (char)(frHex(s[1]) << 4 | frHex(s[2]));
s += 2;
}
if (c != '\0' && c != '\n' && c != '\r') *o++ = c;
}
*o = 0;
}
// "k1=v1&k2=v2" (cut short if the line overflowed). Saves and returns true, or replies with an error.
static inline bool frSet(FrSetting* s, uint8_t n, char* p, bool overflow) {
const char* upd[FR_MAX_KEYS] = {};
while (*p) {
char* amp = strchr(p, '&');
bool last = !amp;
if (amp) *amp = 0;
char* eq = strchr(p, '=');
if (eq) *eq = 0;
if (*p) {
if (last && overflow) {
frReply(F("FR!ERR long "), p);
return false;
}
uint8_t i = 0;
while (i < n && strcmp(s[i].key, p) != 0) i++;
if (i == n) {
frReply(F("FR!ERR unknown "), p);
return false;
}
char* v = eq ? eq + 1 : p + strlen(p); // "key" alone = "key="
frDecode(v);
upd[i] = v;
}
if (last) break;
p = amp + 1;
}
// Build the new block: for each key, the new value or the one already stored.
char next[FR_DATA_MAX + 1];
uint16_t len = 0;
for (uint8_t i = 0; i < n; i++) {
const char* v = upd[i] ? upd[i] : frStored(s[i].key);
if (!v || !*v) continue;
size_t kl = strlen(s[i].key), vl = strlen(v);
if (len + kl + vl + 2 > FR_DATA_MAX) {
frReply(F("FR!ERR long "), s[i].key);
return false;
}
memcpy(next + len, s[i].key, kl);
next[len + kl] = '=';
memcpy(next + len + kl + 1, v, vl + 1);
len += kl + vl + 2;
}
memcpy(frData, next, len);
frData[len] = 0;
frLen = len;
frSave();
return true;
}
// Handles one received line. Returns true once settings were saved (boot carries on).
static inline bool frCommand(FrSetting* s, uint8_t n, char* line, bool overflow) {
if (strcmp(line, "FR!CLEAR") == 0) {
frLen = 0;
frData[0] = 0;
frSave();
Serial.print(F("FR!OK\n"));
return false;
}
if (strncmp(line, "FR!SET", 6) != 0 || (line[6] != ' ' && line[6] != 0)) return false; // not for us
char* p = line + 6;
while (*p == ' ') p++;
if (*p && !frSet(s, n, p, overflow)) return false; // empty FR!SET = carry on, store nothing
Serial.print(F("FR!OK\n"));
return true;
}
// Call right after Serial.begin(115200). Prints the request, waits as described at the top
// (30 s at most), then points every constant in the list at the value to use ("" = not set).
static inline void frSettingsBegin(FrSetting* s, uint8_t n) {
frLoad();
char line[FR_LINE_MAX];
uint16_t len = 0;
bool overflow = false;
frRequest(s, n);
unsigned long start = millis(), quietSince = start;
while (millis() - start < FR_WAIT_MAX_MS) {
int c = Serial.read();
if (c >= 0) {
quietSince = millis();
if (c == '\r') continue;
if (c != '\n') {
if (len < FR_LINE_MAX - 1) line[len++] = (char)c;
else overflow = true;
continue;
}
line[len] = 0;
bool saved = frCommand(s, n, line, overflow);
len = 0;
overflow = false;
if (saved) break;
continue;
}
if (millis() - quietSince < 2000) {
delay(1); // also keeps the ESP8266 watchdog fed
continue;
}
len = 0; // drop half a line
overflow = false;
bool missing = false;
for (uint8_t i = 0; i < n; i++) missing |= frIsMissing(s[i]);
if (!missing) break;
frRequest(s, n);
quietSince = millis();
}
for (uint8_t i = 0; i < n; i++) *s[i].value = frSettingsGet(s[i].key, *s[i].value);
}
template <size_t N>
static inline void frSettingsBegin(FrSetting (&s)[N]) {
static_assert(N <= FR_MAX_KEYS, "fr_settings.h: at most FR_MAX_KEYS settings");
frSettingsBegin(s, (uint8_t)N);
}// fr_settings.h — FlashReady on-board settings (Wi-Fi name, phone number, ...) over USB.
//
// Why: the pre-built file on flashready.in is the same for everyone, so your own settings
// can't be inside it. Instead the board asks for them over USB serial and keeps them in
// its EEPROM. If you use Arduino IDE you can still type them into the constants in the
// sketch — this file then changes nothing except a 2-second pause at start-up.
//
// The protocol, in plain words (one text line each way, ending in \n, at 115200 baud):
// 1. Right after Serial.begin(115200) the board prints
// FR?SETTINGS v1 keys=wifi_ssid,wifi_pass missing=wifi_pass
// "missing" lists the required settings that still have no real value.
// 2. If nothing is missing it listens for 2 seconds and then carries on. If something is
// missing it keeps listening, printing the line again every 2 seconds, for up to
// 30 seconds; then it carries on anyway and the sketch skips whatever needs the
// missing value (an alarm still sounds, it just can't send the SMS).
// 3. The flasher answers FR!SET wifi_ssid=My%20Home&wifi_pass=secret
// (values URL-encoded). The board stores them and answers FR!OK, then carries on with
// the new values. Errors: FR!ERR unknown <key>, FR!ERR long <key> (nothing is stored).
// A key that is left out keeps its stored value; "key=" (empty) removes it.
// "FR!SET" with nothing after it means "carry on now": FR!OK, nothing is stored.
// 4. FR!CLEAR wipes everything that was stored, answers FR!OK and keeps listening.
// Other lines are ignored.
//
// A stored value wins over the constant in the sketch. A constant that is still a
// placeholder ("YOUR_SSID", "your-wifi", "+91XXXXXXXXXX") or empty counts as not set.
//
// EEPROM block at FR_SETTINGS_ADDR: 'F' 'R' 0x01, length (uint16, low byte first), then
// "key=value\n" lines. Uses FR_SETTINGS_SIZE bytes; move FR_SETTINGS_ADDR with a #define
// before the #include if your sketch uses EEPROM for something else.
//
// Use in a sketch:
// const char* WIFI_SSID = "YOUR_SSID";
// FrSetting SETTINGS[] = { {"wifi_ssid", &WIFI_SSID, true} }; // key, constant, required
// void setup() { Serial.begin(115200); frSettingsBegin(SETTINGS); ... WiFi.begin(WIFI_SSID, ...); }
// After frSettingsBegin() each constant points at the value to use ("" if none is set).
#pragma once
#include <Arduino.h>
#include <EEPROM.h>
#include <string.h>
#ifndef FR_SETTINGS_ADDR
#define FR_SETTINGS_ADDR 0
#endif
#ifndef FR_SETTINGS_SIZE
#if defined(__AVR__)
#define FR_SETTINGS_SIZE 96 // 2 KB of RAM on an Uno: a phone number fits easily
#else
#define FR_SETTINGS_SIZE 512
#endif
#endif
#ifndef FR_LINE_MAX
#if defined(__AVR__)
#define FR_LINE_MAX 128
#else
#define FR_LINE_MAX 1024
#endif
#endif
#define FR_MAX_KEYS 8
#define FR_WAIT_MAX_MS 30000UL // never wait longer than this for missing settings
#define FR_DATA_MAX (FR_SETTINGS_SIZE - 5)
struct FrSetting {
const char* key; // [a-z0-9_], as in settings.json
const char** value; // the sketch's constant; replaced by the value to use
bool required;
};
// Stored settings in RAM as "key=value\0key=value\0" (only non-empty values).
static char frData[FR_DATA_MAX + 1];
static uint16_t frLen = 0;
static inline void frEepromBegin() {
#if defined(ESP32) || defined(ESP8266)
static bool started = false;
if (!started) EEPROM.begin(FR_SETTINGS_ADDR + FR_SETTINGS_SIZE);
started = true;
#endif
}
static inline void frLoad() {
frEepromBegin();
frLen = 0;
frData[0] = 0;
if (EEPROM.read(FR_SETTINGS_ADDR) != 'F' || EEPROM.read(FR_SETTINGS_ADDR + 1) != 'R' || EEPROM.read(FR_SETTINGS_ADDR + 2) != 1) return;
uint16_t n = EEPROM.read(FR_SETTINGS_ADDR + 3) | (EEPROM.read(FR_SETTINGS_ADDR + 4) << 8);
if (n > FR_DATA_MAX) return;
for (uint16_t i = 0; i < n; i++) {
char c = EEPROM.read(FR_SETTINGS_ADDR + 5 + i);
frData[i] = c == '\n' ? '\0' : c;
}
frData[n] = 0;
frLen = n;
}
static inline void frPut(int addr, uint8_t b) {
#if defined(__AVR__)
EEPROM.update(addr, b);
#else
EEPROM.write(addr, b);
#endif
}
static inline void frSave() {
frEepromBegin();
frPut(FR_SETTINGS_ADDR, 'F');
frPut(FR_SETTINGS_ADDR + 1, 'R');
frPut(FR_SETTINGS_ADDR + 2, 1);
frPut(FR_SETTINGS_ADDR + 3, frLen & 0xFF);
frPut(FR_SETTINGS_ADDR + 4, frLen >> 8);
for (uint16_t i = 0; i < frLen; i++) frPut(FR_SETTINGS_ADDR + 5 + i, frData[i] ? frData[i] : '\n');
#if defined(ESP32) || defined(ESP8266)
EEPROM.commit();
#endif
}
// The stored value for key, or nullptr.
static inline const char* frStored(const char* key) {
size_t kl = strlen(key);
for (uint16_t i = 0; i < frLen; i += strlen(frData + i) + 1) {
if (strncmp(frData + i, key, kl) == 0 && frData[i + kl] == '=') return frData + i + kl + 1;
}
return nullptr;
}
// Empty, "your_…" / "YOUR-…" / "your …", or a "+91XXXX…" phone number.
static inline bool frIsPlaceholder(const char* v) {
if (!v || !*v) return true;
if (strncasecmp(v, "your", 4) == 0 && (v[4] == '_' || v[4] == '-' || v[4] == ' ')) return true;
return v[0] == '+' && strstr(v, "XXXX") != nullptr;
}
// The stored value for key, else the fallback constant; "" if that is a placeholder.
static inline const char* frSettingsGet(const char* key, const char* fallback) {
const char* v = frStored(key);
if (!v) v = fallback;
return frIsPlaceholder(v) ? "" : v;
}
static inline bool frIsMissing(const FrSetting& s) {
return s.required && !*frSettingsGet(s.key, *s.value);
}
static inline void frRequest(FrSetting* s, uint8_t n) {
Serial.print(F("FR?SETTINGS v1 keys="));
for (uint8_t i = 0; i < n; i++) {
if (i) Serial.print(',');
Serial.print(s[i].key);
}
Serial.print(F(" missing="));
bool first = true;
for (uint8_t i = 0; i < n; i++) {
if (!frIsMissing(s[i])) continue;
if (!first) Serial.print(',');
Serial.print(s[i].key);
first = false;
}
Serial.print('\n');
}
static inline void frReply(const __FlashStringHelper* what, const char* key) {
Serial.print(what);
Serial.print(key);
Serial.print('\n');
}
static inline int8_t frHex(char c) {
if (c >= '0' && c <= '9') return c - '0';
c |= 0x20;
return (c >= 'a' && c <= 'f') ? c - 'a' + 10 : -1;
}
// Percent-decodes in place. Drops NUL, CR and LF: they can't be stored.
static inline void frDecode(char* s) {
char* o = s;
for (; *s; s++) {
char c = *s;
if (c == '%' && frHex(s[1]) >= 0 && frHex(s[2]) >= 0) {
c = (char)(frHex(s[1]) << 4 | frHex(s[2]));
s += 2;
}
if (c != '\0' && c != '\n' && c != '\r') *o++ = c;
}
*o = 0;
}
// "k1=v1&k2=v2" (cut short if the line overflowed). Saves and returns true, or replies with an error.
static inline bool frSet(FrSetting* s, uint8_t n, char* p, bool overflow) {
const char* upd[FR_MAX_KEYS] = {};
while (*p) {
char* amp = strchr(p, '&');
bool last = !amp;
if (amp) *amp = 0;
char* eq = strchr(p, '=');
if (eq) *eq = 0;
if (*p) {
if (last && overflow) {
frReply(F("FR!ERR long "), p);
return false;
}
uint8_t i = 0;
while (i < n && strcmp(s[i].key, p) != 0) i++;
if (i == n) {
frReply(F("FR!ERR unknown "), p);
return false;
}
char* v = eq ? eq + 1 : p + strlen(p); // "key" alone = "key="
frDecode(v);
upd[i] = v;
}
if (last) break;
p = amp + 1;
}
// Build the new block: for each key, the new value or the one already stored.
char next[FR_DATA_MAX + 1];
uint16_t len = 0;
for (uint8_t i = 0; i < n; i++) {
const char* v = upd[i] ? upd[i] : frStored(s[i].key);
if (!v || !*v) continue;
size_t kl = strlen(s[i].key), vl = strlen(v);
if (len + kl + vl + 2 > FR_DATA_MAX) {
frReply(F("FR!ERR long "), s[i].key);
return false;
}
memcpy(next + len, s[i].key, kl);
next[len + kl] = '=';
memcpy(next + len + kl + 1, v, vl + 1);
len += kl + vl + 2;
}
memcpy(frData, next, len);
frData[len] = 0;
frLen = len;
frSave();
return true;
}
// Handles one received line. Returns true once settings were saved (boot carries on).
static inline bool frCommand(FrSetting* s, uint8_t n, char* line, bool overflow) {
if (strcmp(line, "FR!CLEAR") == 0) {
frLen = 0;
frData[0] = 0;
frSave();
Serial.print(F("FR!OK\n"));
return false;
}
if (strncmp(line, "FR!SET", 6) != 0 || (line[6] != ' ' && line[6] != 0)) return false; // not for us
char* p = line + 6;
while (*p == ' ') p++;
if (*p && !frSet(s, n, p, overflow)) return false; // empty FR!SET = carry on, store nothing
Serial.print(F("FR!OK\n"));
return true;
}
// Call right after Serial.begin(115200). Prints the request, waits as described at the top
// (30 s at most), then points every constant in the list at the value to use ("" = not set).
static inline void frSettingsBegin(FrSetting* s, uint8_t n) {
frLoad();
char line[FR_LINE_MAX];
uint16_t len = 0;
bool overflow = false;
frRequest(s, n);
unsigned long start = millis(), quietSince = start;
while (millis() - start < FR_WAIT_MAX_MS) {
int c = Serial.read();
if (c >= 0) {
quietSince = millis();
if (c == '\r') continue;
if (c != '\n') {
if (len < FR_LINE_MAX - 1) line[len++] = (char)c;
else overflow = true;
continue;
}
line[len] = 0;
bool saved = frCommand(s, n, line, overflow);
len = 0;
overflow = false;
if (saved) break;
continue;
}
if (millis() - quietSince < 2000) {
delay(1); // also keeps the ESP8266 watchdog fed
continue;
}
len = 0; // drop half a line
overflow = false;
bool missing = false;
for (uint8_t i = 0; i < n; i++) missing |= frIsMissing(s[i]);
if (!missing) break;
frRequest(s, n);
quietSince = millis();
}
for (uint8_t i = 0; i < n; i++) *s[i].value = frSettingsGet(s[i].key, *s[i].value);
}
template <size_t N>
static inline void frSettingsBegin(FrSetting (&s)[N]) {
static_assert(N <= FR_MAX_KEYS, "fr_settings.h: at most FR_MAX_KEYS settings");
frSettingsBegin(s, (uint8_t)N);
}Compiled for uno on avr 1.8.8 · 3 Nov 2026
How it works
Shop break-ins in India usually happen at night by forcing the rolling shutter, often during a power cut. Commercial alarms with monitoring cost a lot; this one costs a few thousand rupees, runs from its own battery, and tells you immediately — with a siren at the shop and an SMS plus a ringing call on your phone.
How it works
Two sensors watch the shop:
- a reed switch on the shutter. A magnet on the shutter keeps the switch closed while the shutter is down; lifting it opens the switch and D4 reads HIGH through its pull-up;
- a PIR sensor inside, which sees body heat moving — someone who got in through the roof or a wall.
The sketch runs a small state machine:
- DISARMED — press the hidden button to arm.
- EXIT — 60 s with a beep every second, to step out and pull the shutter down. If the shutter is still open at the end, it doesn’t arm (and gives a long beep).
- ARMED — lifting the shutter starts the ENTRY delay (30 s of fast beeps; press the hidden button to disarm). Motion inside with the shutter down is an immediate alarm.
- ALARM — the relay switches the 12 V siren on, and the SIM800L sends an SMS (“SHOP ALARM: shutter opened”) and then calls your phone for 20 seconds so it rings even if you’re asleep. The siren stops after 3 minutes; the alarm can trigger at most three times per arming, so a faulty sensor can’t drain your SIM balance.
Why these parts
- Reed switch + PIR — the shutter is the usual way in; the PIR covers the rest.
- SIM800L — works without Wi-Fi or a router, which is usually dead in a power cut anyway.
- 12 V battery — the alarm’s whole value is working when the power is off.
India notes
Tell your neighbours and the local beat constable about the siren. Test it every month — open the shutter while armed — so you know the SIM still has balance and the battery is still good.
Troubleshooting
It doesn’t arm — beeps for 1.5 s after the exit delay
The shutter was still open (or the reed switch isn’t closing) when the 60 s ended. Check the gap between magnet and switch; the Serial Monitor prints shutter=closed when it’s right.
False alarms at night
The PIR sees a fan, curtains, a pet or sunlight through a vent. Turn its sensitivity trimmer down, point it away from windows, and set its jumper to repeat-trigger. Rats are a real cause in shops — mount it high, pointing down at the floor area near the shutter.
No SMS or call
Check the SIM800L’s LED blinks every 3 s (registered), the SIM has balance, ALERT_TO starts with +91, and the buck gives a steady 4.0 V with the big capacitor fitted.
Upgrades
- Add a second reed switch for the back door or the godown.
- Arm and disarm by SMS from your phone (only from your number).
- Add a small camera module that snaps a photo when the alarm starts.



