ガイド · 読了まで22分

Arduinoでつくるアラームシステム:PIR、ドアセンサー、キーパッド、サイレンのガイド

PIR、リード接点のドアセンサー、振動とレーザーのゾーン、キーパッドの暗証番号、入室・退出遅延、サイレン用リレー、ESP32のTelegram通知を使って、Arduinoのアラームシステムを作ります。

Arduinoのアラームシステムは、自分で作れる防犯装置です。家や部屋に誰かが無断で入るとサイレンを鳴らします。ドアが開いたときは、暗証番号を入力するための数秒間の猶予があります。必要ならスマホにすぐ通知を送ることもできます。このガイドでは、PIR人感センサー、ドアに取り付けるリード接点、窓ガラスへの衝撃を捉える振動センサー、廊下を横切るレーザー侵入警報を使って、4ゾーンのアラームを作ります。警戒と解除は4x4キーパッドの暗証番号で行い、12 Vのサイレンはリレー経由で鳴らします。これらをすべて1台のArduino Unoで動かします。

市販のアラーム制御盤が行っている機能を、小さな形でステップごとに再現していきます。入室・退出遅延、在宅モード(夜間モード)、暗証番号の誤入力によるロックアウト、サイレンの鳴動時間の上限、アラーム履歴です。コードは入れ子のifブロックではなくステートマシンとして書きます。そのため、アラームがその瞬間に何をしているのかが常にはっきりします。そのあと同じロジックをESP32に移し、Telegramでの警戒・解除とアラーム通知を追加します。さらにRaspberry Pi Pico向けのMicroPython版を書き、いちばん厄介な問題である誤報を減らす方法も紹介します。

HC-SR501 PIR人感センサー
HC-SR501 PIR人感センサーは、アラームの室内ゾーンを担当します。白いフレネルレンズが視野を数十の小さなゾーンに分けます。横にある2つの半固定抵抗で、検知距離と出力時間を設定します。

Arduinoのアラームシステムの仕組み

どのアラームシステムも、3つの部分でできています。入力(センサー)、判断する頭脳(Arduino)、出力(サイレン、ブザー、通知)です。各センサーはそれぞれ1つのゾーンを守ります。プロ用の制御盤では、作動したときの動き方によってゾーンを分類します。よいアラームと、ドアが開くたびに鳴る迷惑な装置を分けるのが、この区別です。今回のシステムには4つのゾーンがあります。

ゾーンセンサーゾーンの種類警戒中に作動した場合
玄関ドアリード接点(MC-38)遅延20秒の入室遅延を開始。暗証番号が入力されなければアラーム
リビング、室内PIR(HC-SR501)室内(フォロワー)即時アラーム。入室遅延中は待機。在宅モードでは無効
窓振動(SW-420)即時3秒以内に3回の衝撃で即時アラーム
廊下、階段レーザー+LDR(KY-008)室内(フォロワー)即時アラーム。入室遅延中は待機
ガス、火災、タンパーMQ-2、炎センサー、ケースのスイッチ24時間警戒解除中でもアラーム(拡張のアイデア)

遅延ゾーンがあるので、帰宅した瞬間にサイレンが鳴ることはありません。ドアが開くとブザーが断続的に鳴って知らせ、暗証番号を入力するまで20秒の猶予があります。室内のフォロワーゾーンは、入室遅延中は待ってくれます。そのため、ドアからキーパッドまで歩く途中でPIRの前を通っても、アラームは鳴りません。一方、ドアを開けずに窓などから侵入してPIRの前に出た場合は、入室遅延が始まっていないので、すぐにアラームが鳴ります。即時ゾーンは、どんな場合でもすぐに作動します。窓をこじ開けようとしている相手に20秒も与える意味はないからです。外出時も同じような考え方です。警戒をセットすると30秒の退出遅延が始まり、その間にドアから出てもアラームは作動しません。

Arduinoのアラームシステムの部品リスト

部品数量役割
Arduino Uno(またはNano)1システムの頭脳。このガイドのピンはUnoとNanoで共通
HC-SR501 PIR人感センサー1室内の動きを検知。3〜7 m、約110°
MC-38リード接点のドアセンサー(またはリードスイッチ+磁石)1ドアや窓が開くと回路を開く
SW-420振動センサーモジュール1ガラスや窓枠への衝撃を検知
KY-008レーザーモジュール+LDR+10 kΩの抵抗1セット廊下を横切る光のバリア
4x4メンブレンキーパッド1暗証番号による警戒と解除
アクティブブザー(5 V)1キーの操作音、入室・退出の警告、室内サイレン
1チャンネルの5 Vリレーモジュール1Arduinoから絶縁して12 Vのサイレンを切り替える
12 Vのサイレン(できればストロボ付き)1メインのアラーム音。通常100〜120 dB
赤と緑のLED+220 Ωの抵抗2本1セット警戒中と準備完了の表示
12 V電源、LM2596モジュール、12 Vバッテリー(任意)1セット電源と停電時のバックアップ
ブレッドボード、ジャンパーワイヤー、4芯の防犯用ケーブル―配線と離れた場所のセンサー
Arduino Uno
Arduino Uno
リードスイッチ
リードスイッチ
SW-420
SW-420
KY-008
KY-008
4x4キーパッド
4x4キーパッド
ブザー
ブザー
5 Vリレー
5 Vリレー

センサー:各ゾーンは何を検知するのか

  • PIR(HC-SR501):人体が出す赤外線の変化を検知し、3〜7メートルの距離、約110度の範囲で動きを捉えます。出力は3.3 Vですが、電源は5 Vが必要です。電源投入後の30〜60秒間は周囲を学習している最中で、勝手に作動することがあります。そのため、コードでは最初の60秒間を無視しています。詳しくはPIR人感センサーの記事を参照してください。
  • リード接点のドアセンサー(MC-38):2つのプラスチックケースでできていて、1つは枠に、もう1つはドア本体にネジで固定します。ドアが閉まっていると磁石が近くにあり、接点は閉じています。ドアが15〜25 mm以上開くと接点が開きます。1本のピンと内部プルアップで読み取れます。中の仕組みはリードスイッチの記事で説明しています。
  • 振動センサー(SW-420):ばねに囲まれた金属棒が入った、常時閉のスイッチです。ガラスや窓枠に衝撃が加わると接点が一瞬開き、基板上のLM393コンパレーターがそれをデジタルのパルスに変えます。ほとんどのモジュールでは、静止時の出力はLOWで、衝撃を受けると短時間HIGHになります。手元のモジュールの動きは、センサーテストで確認してください。感度は基板上の半固定抵抗で調整します。詳しくはSW-420振動センサーの記事にあります。
  • レーザー侵入警報(KY-008+LDR):波長650 nm、約5 mWのレーザーが、廊下の反対側にあるLDRを常に照らします。ビームが遮られると、LDRの抵抗は数百オームから数十キロオームに跳ね上がり、アナログの読み取り値が大きく下がります。設置と光軸合わせのコツはレーザーモジュールの記事にあります。
  • 4x4キーパッド:4本の行ラインと4本の列ラインでできた、16キーのマトリクスです。使うピンは8本で、Keypadライブラリを使えば1行で読み取れます。行と列のスキャンの仕組みはキーパッドのガイドで紹介しています。
  • サイレンとブザー:アクティブブザーは発振回路を内蔵しているので、ピンをHIGHにするだけで鳴ります。キーの操作音や入室・退出の警告にぴったりです。本当に威嚇になる音は12 Vのサイレンが担当し、リレーモジュール経由で鳴らします。アクティブブザーとパッシブブザーの違いは「ブザーとは?」を参照してください。
KY-008はクラス3Rのレーザーです。ビームを直接、または鏡越しに見ると目を傷める恐れがあります。ビームは必ず目の高さより下、たとえば床から20〜40 cmの高さに通してください。この高さなら安全で、しかも侵入した人が歩くときに確実にビームを遮ります。

配線図とピン表

次の表は、3種類のボードの配線をまとめたものです。Arduino Uno版は、4つのゾーンとキーパッドをすべて使います。ESP32版はキーパッドの代わりにTelegramのコマンドで警戒・解除し、Pico版は短くまとめているため、レーザー侵入警報はUno版だけにあります。手元のボードのピン番号は「ピン配置」ツールで、LEDの抵抗値はLED抵抗計算ツールで確認できます。

接続Arduino UnoESP32Raspberry Pi Pico
キーパッドR1〜R4(行)D9, D8, D7, D6―(Telegram)GP2, GP3, GP4, GP5
キーパッドC1〜C4(列)D5, D4, D3, D2―(Telegram)GP6, GP7, GP8, GP9
PIR VCC / OUT / GND5V / D10 / GNDVIN (5V) / GPIO 27 / GNDVBUS / GP16 / GND
リード接点(2本の線)D11とGNDGPIO 33とGNDGP15とGND
SW-420 VCC / DO / GND5V / D12 / GND3V3 / GPIO 32 / GND3V3(OUT) / GP17 / GND
KY-008 S / −(レーザーは常時点灯)5V / GND――
LDRは5VとA0の間、10 kΩはA0とGNDの間A0――
アクティブブザーの+(−はGND)A1GPIO 25GP14
リレーモジュールのINA2GPIO 26GP13
緑のLED(220 Ω付き):準備完了A3――
赤のLED(220 Ω付き):警戒中D13GPIO 2(内蔵LED)内蔵LED
12 Vのサイレンは、絶対にArduinoのピンへ直接つながないでください。電源のプラスをリレーのCOM端子に、リレーのNO端子をサイレンのプラスに、サイレンのマイナスを電源のマイナスに戻します。こうすれば、サイレンの電流がArduinoを通ることはありません。ESP32やPicoの3.3 Vのピンでは、一部の5 Vリレーモジュールを完全にオフにできず、リレーが入ったままになることがあります。これらのボードでは、3.3 V対応またはHIGHトリガーのモジュールを使うか、NPNトランジスタを使ったドライバを使ってください。

アラームがいちばん役に立つのは停電のときです。電源は最初から考えておきましょう。実用的な構成を紹介します。プロ用の制御盤と同じように、バッテリーを接続できる13.8 Vのアラーム用電源を使います。この電源でサイレンを動かし、さらにLM2596降圧コンバーターで5 Vに下げて、Arduinoの5Vピンにも供給します。停電すると、12 V 7 Ahの密閉型鉛蓄電池に切り替わります。計算は簡単です。待機中のUno、PIR、レーザー、LED、SW-420は、5 Vで合計約100 mAを消費します。これは0.5 Wで、効率85 %のコンバーターを使うと、バッテリー側では約50 mAになります。容量の80 %しか使わないとしても、7 Ahのバッテリーなら4〜5日間システムを動かせます。サイレンが鳴っている間は0.2〜0.5 Aを消費するので、その分だけ少し短くなります。手元の値で試すには電池寿命計算ツールを、電源の選択肢については「Arduinoの電源の取り方」を参照してください。

ステップごとの組み立て

  1. Arduino IDEでライブラリマネージャを開き、Keypadと入力して、Mark Stanley氏とAlexander Brevig氏のライブラリをインストールします。インストールしていないと、コンパイル時にKeypad.h: No such file or directory(ファイルまたはディレクトリがない)というエラーが出ます。
  2. キーパッドをキーが手前に見える向きで持ちます。左から最初の4本の線(R1〜R4)をD9, D8, D7, D6に、次の4本(C1〜C4)をD5, D4, D3, D2につなぎます。
  3. PIRを5V、D10、GNDにつなぎます。2つの半固定抵抗はどちらも反時計回りにいっぱいまで回し、トリガー設定のジャンパーをHにします。
  4. リード接点の2本の線をD11とGNDに、SW-420を5V、D12、GNDにつなぎます。
  5. LDRを5VとA0の間に、10 kΩの抵抗をA0とGNDの間に挿します。LDRは黒い筒の奥に入れ、レーザーが筒のちょうど中心に当たるように光軸を合わせます。
  6. ブザーをA1に、リレーモジュールのINをA2につなぎ、VCCとGNDはArduinoの5VとGNDにつなぎます。LEDは220 Ωの抵抗を通してA3とD13につなぎます。
  7. まず下のセンサーテストを書き込み、シリアルモニタを9600ボーで開いて、センサーを1つずつ作動させます。
  8. 次にアラームのスケッチ全体を書き込み、暗証番号を変更します。サイレンをつなぐ前に、数日間ブザーだけでシステムを試してください。

最初のテスト:すべてのセンサーをシリアルモニタで確認する

アラームのコードに進む前に、各センサーが思いどおりに動くか確認しましょう。この小さなスケッチは、0.5秒ごとに4つの情報を表示します。PIRの状態、ドアが開いているか閉じているか、直近0.5秒間の振動パルスの数、LDRのアナログ値です。振動のピンは常に読み続けています。SW-420のパルスは数ミリ秒しか続かないことがあり、途中でdelay()を使うコードでは取りこぼしてしまうからです。

C++ (Arduino IDE)
1// Alarm sensör testi: tüm girişleri saniyede iki kez yazdırır / Alarm sensor test: prints every input twice a second
2const int PIR_PIN = 10;        // HC-SR501 OUT
3const int KAPI_PIN = 11;       // reed kapı kontağı, diğer ucu GND / reed door contact, other leg to GND
4const int TITRESIM_PIN = 12;   // SW-420 DO
5const int LAZER_PIN = A0;      // LDR bölücünün orta noktası / LDR divider midpoint
6
7int titresimSayisi = 0;        // yarım saniyedeki darbe sayısı / pulses in the last half second
8int oncekiTitresim = LOW;
9unsigned long sonYazma = 0;
10
11void setup() {
12  Serial.begin(9600);
13  pinMode(PIR_PIN, INPUT);
14  pinMode(KAPI_PIN, INPUT_PULLUP);   // kapı kapalı = LOW, açık = HIGH / door closed = LOW, open = HIGH
15  pinMode(TITRESIM_PIN, INPUT);
16}
17
18void loop() {
19  int t = digitalRead(TITRESIM_PIN);  // kısa darbeleri kaçırmamak için sürekli oku / read constantly so short pulses are not missed
20  if (t == HIGH && oncekiTitresim == LOW) titresimSayisi++;
21  oncekiTitresim = t;
22
23  if (millis() - sonYazma >= 500) {
24    sonYazma = millis();
25    Serial.print(F("PIR: "));
26    Serial.print(digitalRead(PIR_PIN));
27    Serial.print(F("  Kapi / door: "));
28    Serial.print(digitalRead(KAPI_PIN) == HIGH ? F("ACIK / open") : F("kapali / closed"));
29    Serial.print(F("  Titresim / vibration: "));
30    Serial.print(titresimSayisi);
31    Serial.print(F("  Lazer / laser: "));
32    Serial.println(analogRead(LAZER_PIN));
33    titresimSayisi = 0;
34  }
35}
このテストで、レーザーのしきい値も決められます。ビームがLDRに当たっているときの読み取り値(例:950)と、手でビームを遮ったときの値(例:250)をメモします。アラームのコードのLAZER_ESIK(esikはトルコ語で「しきい値」)を、その中間(600)に設定してください。2つの値の差が300未満なら、筒を長くするか、レーザーの光軸を合わせ直します。

ステートマシン:アラームの頭脳

アラームをifブロックとdelay()だけで書こうとすると、すぐに手に負えないコードになります。入室遅延を待っている間はキーパッドを読めず、サイレンが鳴っている間は暗証番号を入力できません。解決策はステートマシンです。システムは常にどれか1つの状態にあり、決まったイベントが起きたときだけ別の状態に移ります。コードでは状態をenumで定義します。loop()は毎回、今の状態のルールだけを実行し、待ち時間はdelay()ではなくmillis()で計ります。こうすればループは1秒間に何千回も回り、キー入力もセンサーのイベントも取りこぼしません。

状態入る条件動作抜ける条件
解除起動時と、暗証番号 + #の入力後センサーを監視するがアラームは出さない。すべて準備完了なら緑のLEDが点灯暗証番号 + Aまたは暗証番号 + B
退出遅延暗証番号 + A(外出)またはB(在宅)30秒をカウントダウン。ブザーは1秒に1回鳴り、最後の10秒で速くなる時間切れで警戒へ。暗証番号 + #で解除へ
警戒退出遅延が終わったときすべてのゾーンを監視。赤のLEDが点灯し続けるドアが開くと入室遅延へ。即時ゾーンか室内ゾーンが作動するとアラームへ
入室遅延警戒中にドアが開いたとき20秒間ブザーが断続的に鳴る。PIRとレーザーは保留暗証番号 + #で解除へ。時間切れでアラームへ
アラームゾーンが作動したときサイレンとブザーが鳴る。サイレンは3分後に止まり、赤のLEDが速く点滅暗証番号 + #でのみ解除へ

キーパッドでの警戒・解除と暗証番号

キー機能
2580 + A外出モード:4つのゾーンがすべて有効、30秒の退出遅延
2580 + B在宅(夜間)モード:PIRは無効。ドア、窓、レーザーは有効
2580 + #解除。アラームがあった場合は、作動したゾーンをシリアルモニタに表示
*またはC入力を間違えた番号を消去
Dゾーンテスト:ドア、PIR、レーザーの値をシリアルモニタに表示
3回の誤入力キーパッドが60秒間ロックされる。この間は正しい暗証番号も受け付けない

警戒をセットするときにも暗証番号を求めるので、子どもや来客がうっかりセットしてしまうことを防げます。コードは準備未完了のチェックも行います。ドアが開いている、またはレーザーのビームが遮られている状態でセットしようとすると、長いビープ音が鳴ってセットされません。市販の制御盤と同じ動きです。暗証番号は初期値の2580のままにせず、1234、0000、生まれ年のような推測しやすい番号も避けてください。スケッチを書き込み直さずに暗証番号を変えたい場合は、番号をEEPROMに保存し、キーパッドから新しい番号を入力できるメニューを追加します。キーパッドの横にI2C LCDディスプレイを付ければ、カウントダウンや状態のメッセージも画面で確認できます。A4とA5のピンを空けてあるのはこのためです。

Arduinoのアラームシステムのコード

次のコードが、このガイドの中心です。冒頭の設定部分で、暗証番号、遅延時間、サイレンの鳴動時間、レーザーのしきい値、リレーの種類を変更できます。それ以外の部分は触る必要はありません。Arduino Unoでは、プログラムメモリを約6 KB、RAMを620バイト使います。LCDやRFIDリーダーを追加する余裕は十分にあります。

C++ (Arduino IDE)
1// Arduino ev alarm sistemi: 4 bölge, keypad, giriş/çıkış gecikmesi, siren / Arduino home alarm: 4 zones, keypad, entry/exit delay, siren
2#include <Keypad.h>
3
4// ---- Ayarlar / Settings ----
5const char SIFRE[] = "2580";                 // kendi şifreni yaz / set your own code
6const unsigned long CIKIS_SURE = 30000;      // çıkış gecikmesi 30 sn / exit delay 30 s
7const unsigned long GIRIS_SURE = 20000;      // giriş gecikmesi 20 sn / entry delay 20 s
8const unsigned long SIREN_SURE = 180000;     // siren en fazla 3 dk çalar / siren sounds for at most 3 min
9const unsigned long PIR_ISINMA = 60000;      // PIR ilk 60 sn yok sayılır / PIR is ignored for the first 60 s
10const unsigned long KILIT_SURE = 60000;      // 3 yanlış şifrede 60 sn kilit / 60 s lockout after 3 wrong codes
11const int LAZER_ESIK = 600;                  // ışınlı ve ışınsız değerin ortası / midpoint of beam and no-beam readings
12const bool LAZER_VAR = true;                 // lazer bariyerin yoksa false yap / set to false without a laser barrier
13const bool ROLE_AKTIF_LOW = true;            // çoğu röle modülü LOW'da çeker / most relay modules switch on with LOW
14
15// ---- Pinler / Pins ----
16const int PIR_PIN = 10;        // HC-SR501 OUT
17const int KAPI_PIN = 11;       // reed, diğer ucu GND / reed, other leg to GND
18const int TITRESIM_PIN = 12;   // SW-420 DO
19const int LAZER_PIN = A0;      // LDR bölücü / LDR divider
20const int BUZZER_PIN = A1;     // aktif buzzer / active buzzer
21const int ROLE_PIN = A2;       // siren rölesi / siren relay
22const int YESIL_LED = A3;      // hazır / ready
23const int KIRMIZI_LED = 13;    // kurulu / armed
24
25const byte SATIR = 4, SUTUN = 4;
26char tuslar[SATIR][SUTUN] = {
27  {'1', '2', '3', 'A'},
28  {'4', '5', '6', 'B'},
29  {'7', '8', '9', 'C'},
30  {'*', '0', '#', 'D'}
31};
32byte satirPinleri[SATIR] = {9, 8, 7, 6};     // R1–R4
33byte sutunPinleri[SUTUN] = {5, 4, 3, 2};     // C1–C4
34Keypad keypad = Keypad(makeKeymap(tuslar), satirPinleri, sutunPinleri, SATIR, SUTUN);
35
36enum Durum { COZULU, CIKIS_GECIKMESI, KURULU, GIRIS_GECIKMESI, ALARM };
37const char* const DURUM_ADI[] = { "COZULU / DISARMED", "CIKIS / EXIT DELAY", "KURULU / ARMED", "GIRIS / ENTRY DELAY", "ALARM" };
38
39struct Giris {                 // gürültü süzgeçli giriş / debounced input
40  bool ham;
41  bool kararli;
42  unsigned long degisim;
43};
44Giris gKapi = {false, false, 0};
45Giris gPir = {false, false, 0};
46Giris gLazer = {false, false, 0};
47
48Durum durum = COZULU;
49unsigned long durumZamani = 0;     // duruma giriş anı / when the state began
50bool evdeModu = false;             // true: PIR devre dışı / true: PIR bypassed
51bool sirenAcik = false;
52bool pirHazir = false;
53const char* tetikleyen = "";       // alarmı başlatan bölge / zone that caused the alarm
54char girilen[9] = "";              // girilen şifre, en fazla 8 hane / typed code, max 8 digits
55byte haneSayisi = 0;
56byte yanlisSayisi = 0;
57bool kilitli = false;
58unsigned long kilitZamani = 0;
59unsigned long bipZamani = 0, bipSuresi = 0;
60unsigned long darbe[3] = {0, 0, 0};          // son 3 titreşim olayı / last 3 vibration events
61int oncekiTitresim = LOW;
62
63// Okuma "sure" ms boyunca aynı kalırsa kabul edilir / a reading is accepted once it stays the same for "sure" ms
64bool suz(Giris &g, bool okuma, unsigned long sure) {
65  unsigned long simdi = millis();
66  if (okuma != g.ham) {
67    g.ham = okuma;
68    g.degisim = simdi;
69  }
70  if (simdi - g.degisim >= sure) g.kararli = g.ham;
71  return g.kararli;
72}
73
74// 3 sn içinde 3 ayrı darbe = zorlama; tek vuruş alarm vermez / 3 separate hits within 3 s = forcing; a single knock is ignored
75bool titresimVar(unsigned long simdi) {
76  int okuma = digitalRead(TITRESIM_PIN);
77  bool sonuc = false;
78  if (okuma == HIGH && oncekiTitresim == LOW && simdi - darbe[2] > 150) {   // 150 ms içindeki sekmeler tek olay / bounces within 150 ms count once
79    darbe[0] = darbe[1];
80    darbe[1] = darbe[2];
81    darbe[2] = simdi;
82    if (darbe[0] != 0 && simdi - darbe[0] <= 3000) sonuc = true;
83  }
84  oncekiTitresim = okuma;
85  return sonuc;
86}
87
88void bip(unsigned long ms) {       // kısa = tuş sesi, uzun = hata / short = key click, long = error
89  bipZamani = millis();
90  bipSuresi = ms;
91}
92
93void sirenYaz(bool acik) {
94  sirenAcik = acik;
95  if (ROLE_AKTIF_LOW) digitalWrite(ROLE_PIN, acik ? LOW : HIGH);
96  else                digitalWrite(ROLE_PIN, acik ? HIGH : LOW);
97}
98
99void durumDegis(Durum yeni) {
100  durum = yeni;
101  durumZamani = millis();
102  Serial.print(F("Durum / state: "));
103  Serial.println(DURUM_ADI[yeni]);
104}
105
106void alarmVer(const char* bolge) {
107  tetikleyen = bolge;
108  sirenYaz(true);
109  durumDegis(ALARM);
110  Serial.print(F("ALARM! Bolge / zone: "));
111  Serial.println(bolge);
112}
113
114bool kurulabilirMi() {             // açık kapı ya da kesik ışınla kurulmaz / never arm with an open door or a broken beam
115  if (gKapi.kararli) {
116    Serial.println(F("Kapi acik, kurulamaz / door open, cannot arm"));
117    return false;
118  }
119  if (LAZER_VAR && gLazer.kararli) {
120    Serial.println(F("Lazer kesik, kurulamaz / beam broken, cannot arm"));
121    return false;
122  }
123  return true;
124}
125
126void kur(bool evde) {
127  if (!kurulabilirMi()) {
128    bip(800);
129    return;
130  }
131  evdeModu = evde;
132  for (int i = 0; i < 3; i++) darbe[i] = 0;
133  tetikleyen = "";
134  durumDegis(CIKIS_GECIKMESI);
135  Serial.println(evde ? F("Evde modu: PIR devre disi / stay mode: PIR bypassed") : F("Tam kurulum / away mode"));
136}
137
138void coz() {
139  sirenYaz(false);
140  if (tetikleyen[0] != '\0') {     // alarm hafızası / alarm memory
141    Serial.print(F("Alarm hafizasi / alarm memory: "));
142    Serial.println(tetikleyen);
143  }
144  durumDegis(COZULU);
145  bip(150);
146}
147
148void sensorRaporu() {
149  Serial.print(F("Kapi / door: "));
150  Serial.print(gKapi.kararli ? F("ACIK / open") : F("kapali / closed"));
151  Serial.print(F("  PIR: "));
152  Serial.print(digitalRead(PIR_PIN));
153  Serial.print(F("  Lazer / laser: "));
154  Serial.println(analogRead(LAZER_PIN));
155}
156
157void tusIsle(char t) {
158  if (t >= '0' && t <= '9') {
159    if (haneSayisi < 8) {
160      girilen[haneSayisi++] = t;
161      girilen[haneSayisi] = '\0';
162    }
163    bip(30);
164    return;
165  }
166  if (t == '*' || t == 'C') {      // girilen kodu sil / clear the typed code
167    haneSayisi = 0;
168    girilen[0] = '\0';
169    bip(30);
170    return;
171  }
172  if (t == 'D') {                  // bölge testi / zone test
173    sensorRaporu();
174    return;
175  }
176
177  // A, B ve # önce şifre ister / A, B and # need the code first
178  bool dogru = strcmp(girilen, SIFRE) == 0;
179  haneSayisi = 0;
180  girilen[0] = '\0';
181  if (kilitli) {
182    Serial.println(F("Tus takimi kilitli / keypad locked"));
183    bip(800);
184    return;
185  }
186  if (!dogru) {
187    Serial.println(F("Yanlis sifre / wrong code"));
188    if (++yanlisSayisi >= 3) {
189      kilitli = true;
190      kilitZamani = millis();
191      yanlisSayisi = 0;
192    }
193    bip(800);
194    return;
195  }
196  yanlisSayisi = 0;
197  if (durum == COZULU && t == 'A')      kur(false);   // tam kurulum / away
198  else if (durum == COZULU && t == 'B') kur(true);    // evde modu / stay
199  else if (durum != COZULU && t == '#') coz();        // çöz / disarm
200  else bip(800);
201}
202
203void cikislariGuncelle() {
204  unsigned long simdi = millis();
205  unsigned long gecen = simdi - durumZamani;
206  bool buzzer = simdi - bipZamani < bipSuresi;   // tuş sesi / key click
207  bool kirmizi = false;
208
209  switch (durum) {
210    case CIKIS_GECIKMESI: {
211      unsigned long periyot = (gecen > CIKIS_SURE - 10000) ? 250 : 1000;   // son 10 sn hızlanır / faster in the last 10 s
212      buzzer = buzzer || (gecen % periyot) < 60;
213      kirmizi = (gecen % 1000) < 500;
214      break;
215    }
216    case KURULU:
217      kirmizi = true;
218      break;
219    case GIRIS_GECIKMESI:
220      buzzer = buzzer || (gecen % 400) < 200;    // "şifreni gir" uyarısı / "enter your code" warning
221      kirmizi = (gecen % 400) < 200;
222      break;
223    case ALARM:
224      buzzer = buzzer || sirenAcik;
225      kirmizi = (gecen % 200) < 100;             // hızlı yanıp sönme = alarm hafızası / fast blink = alarm memory
226      break;
227    default:
228      break;
229  }
230  bool hazir = !gKapi.kararli && !(LAZER_VAR && gLazer.kararli);
231  digitalWrite(BUZZER_PIN, buzzer ? HIGH : LOW);
232  digitalWrite(KIRMIZI_LED, kirmizi ? HIGH : LOW);
233  digitalWrite(YESIL_LED, (durum == COZULU && hazir) ? HIGH : LOW);
234}
235
236void setup() {
237  Serial.begin(9600);
238  sirenYaz(false);                   // pin çıkış olmadan önce "kapalı" yaz, açılışta röle çekmez / write "off" first so the relay doesn't click at boot
239  pinMode(ROLE_PIN, OUTPUT);
240  pinMode(BUZZER_PIN, OUTPUT);
241  pinMode(KIRMIZI_LED, OUTPUT);
242  pinMode(YESIL_LED, OUTPUT);
243  pinMode(PIR_PIN, INPUT);
244  pinMode(KAPI_PIN, INPUT_PULLUP);   // kapı kapalı = LOW; kablo kesilirse HIGH / door closed = LOW; a cut wire reads HIGH
245  pinMode(TITRESIM_PIN, INPUT);
246  keypad.setDebounceTime(20);
247  durumDegis(COZULU);
248  Serial.println(F("PIR 60 sn isiniyor / PIR warming up for 60 s"));
249}
250
251void loop() {
252  char t = keypad.getKey();
253  if (t) tusIsle(t);
254
255  unsigned long simdi = millis();
256  if (kilitli && simdi - kilitZamani >= KILIT_SURE) kilitli = false;
257  if (!pirHazir && simdi >= PIR_ISINMA) pirHazir = true;
258
259  bool kapiAcik = suz(gKapi, digitalRead(KAPI_PIN) == HIGH, 50);
260  bool hareket = suz(gPir, digitalRead(PIR_PIN) == HIGH, 200) && pirHazir;
261  bool isinKesik = LAZER_VAR && suz(gLazer, analogRead(LAZER_PIN) < LAZER_ESIK, 30);
262  bool titresim = titresimVar(simdi);
263
264  switch (durum) {
265    case CIKIS_GECIKMESI:
266      if (simdi - durumZamani >= CIKIS_SURE) durumDegis(KURULU);
267      break;
268    case KURULU:                                   // anlık bölgeler önce / instant zones first
269      if (titresim)                  alarmVer("Pencere (titresim) / window (vibration)");
270      else if (isinKesik)            alarmVer("Lazer bariyeri / laser beam");
271      else if (hareket && !evdeModu) alarmVer("Ic mekan (PIR) / interior (PIR)");
272      else if (kapiAcik)             durumDegis(GIRIS_GECIKMESI);
273      break;
274    case GIRIS_GECIKMESI:                          // PIR ve lazer giriş süresince bekler / PIR and laser wait during the entry delay
275      if (titresim)                                 alarmVer("Pencere (titresim) / window (vibration)");
276      else if (simdi - durumZamani >= GIRIS_SURE)   alarmVer("Giris kapisi / entry door");
277      break;
278    case ALARM:
279      if (sirenAcik && simdi - durumZamani >= SIREN_SURE) {
280        sirenYaz(false);                           // siren susar, hafıza kalır / siren stops, memory stays
281        Serial.println(F("Siren sustu, sifre bekleniyor / siren off, waiting for the code"));
282      }
283      break;
284    default:
285      break;
286  }
287  cikislariGuncelle();
288}

コードの中心になるのがsuz()関数(トルコ語で「ふるいにかける」)です。各入力の生の値が変わるとタイムスタンプを記録します。値が一定時間(ドアは50 ms、PIRは200 ms、レーザーは30 ms)変わらずに続くまで、安定した状態は更新しません。この単純なフィルタによって、長いケーブルに乗る短いノイズや接点のチャタリングでアラームが鳴るのを防げます。ドアのピンをINPUT_PULLUPで読むのにも理由があります。ドアが閉まっているとリードスイッチがピンをGNDにつなぎ、LOWになります。ドアが開くか、誰かがケーブルを切ると、ピンはHIGHになります。実際のアラームシステムで使われているこの閉ループの考え方では、断線もドアが開いたのと同じ扱いになります。

振動は別の方法で処理します。SW-420は1回たたいただけでも、数ミリ秒の間に何度もチャタリングします。そこでtitresimVar()(titresimはトルコ語で「振動」)は、まず150 ms以内のチャタリングを1回のイベントとして数え、直近3回のイベントの時刻を記録します。3回のイベントが3秒以内に収まれば、本当に窓をこじ開けようとしていると判断します。風でドアが揺れたり、ボールが1回ガラスに当たったりしても、アラームは鳴りません。サイレンは3分後に自動で止まります。それでもシステムはアラーム状態のままで、赤のLEDが速く点滅し、帰宅した人にアラームがあったことを知らせます。正しい暗証番号を入力すると、tetikleyen(トルコ語で「作動させたもの」)変数に保存されたゾーン名がシリアルモニタに表示されます。市販の制御盤にあるアラーム履歴の小さな再現です。

入室・退出遅延の決め方

遅延が短すぎると自分でアラームを鳴らしてしまい、長すぎると侵入者に時間を与えてしまいます。退出遅延は、キーパッドからドアまで歩いて鍵をかけるのにかかる時間の2倍にします。入室遅延は、ドアを開け、キーパッドまで歩いて暗証番号を入力するのにかかる時間の約2倍にします。市販の制御盤では、退出は30〜60秒、入室は15〜30秒が一般的です。キーパッドをドアのすぐ横の、外からは見えない場所に取り付ければ、入室遅延を短くできます。ブザーの鳴り方も大切です。退出時は1秒ごとに短く鳴り、最後の10秒で速くなるので、時間が迫っていることが感覚でわかります。入室時は200 msの断続音で、暗証番号の入力を促します。

サイレンとリレーの配線

サイレンの回路では、Arduinoが動かすのはリレーのコイルだけです。12 Vのサイレンの電流は、リレーの接点を流れます。リレーモジュールの多くはアクティブLOWで、INピンをLOWにするとオンになります。コードにROLE_AKTIF_LOWという定数があるのはこのためです。コードでは、ピンを出力に設定する前にオフのレベルを書き込みます。こうしておけば、Arduinoの起動時にリレーが一瞬入ってサイレンが鳴ることはありません。サイレンを選ぶときは電流値を確認してください。110 dBの小型12 Vサイレンは、通常0.1〜0.3 Aを消費します。10 Aのリレー接点にとってはごく小さな負荷です。ストロボ付きの屋外用サイレンは、音と光の両方で威嚇でき、どこでアラームが鳴っているのかを近所の人にも知らせます。プロ用の制御盤は、たいてい3分後にサイレンを止めます。国によっては法律で上限が決まっていることもあるので、SIREN_SURE(sureはトルコ語で「時間」)は常識的な値にしてください。リレーの代わりに半導体スイッチを使いたい場合は、ロジックレベルMOSFETが使えます。コイル式(電磁式)のサイレンでは、端子間に逆向きのダイオードを追加してください。

ESP32でTelegram通知

誰にも聞こえないサイレンでは意味がありません。外出中にアラームが鳴ったことを知るいちばん簡単な方法が、ESP32とTelegramです。ESP32はWi-Fiにつながり、暗号化された接続でTelegramのサーバーにメッセージを送ります。こちらからコマンドを送ることもできます。BotFatherでのボットの作成、トークンとチャットIDの取得、UniversalTelegramBotとArduinoJsonのライブラリのインストールは、「ESP32のTelegramボット」の記事でステップごとに説明しています。この版にはキーパッドはありません。スマホから/kur(外出警戒)、/evde(在宅モード)、/coz(解除)、/durum(状態)のコマンド(いずれもトルコ語)で操作します。ボットは登録したチャットIDにだけ応答し、ほかの人からのコマンドは黙って無視します。

C++ (Arduino IDE)
1// ESP32 alarm sistemi + Telegram: kurma/çözme komutları ve anlık bildirim / ESP32 alarm + Telegram: arm/disarm commands and instant alerts
2#include <WiFi.h>
3#include <WiFiClientSecure.h>
4#include <UniversalTelegramBot.h>        // ArduinoJson kütüphanesi de gerekir / also needs ArduinoJson
5#include <Preferences.h>                 // kurulu durumu elektrik kesintisinde unutmamak için / remembers the armed state across power cuts
6
7const char* WIFI_SSID = "AgAdi";         // Wi-Fi adın (yalnız 2,4 GHz) / your Wi-Fi name (2.4 GHz only)
8const char* WIFI_PASS = "Sifre123";      // Wi-Fi şifren / your Wi-Fi password
9#define BOT_TOKEN "1234567890:AAExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"   // BotFather'dan / from BotFather
10#define CHAT_ID   "123456789"            // yalnız bu ID komut verebilir / only this ID may send commands
11
12const int PIR_PIN = 27;                  // HC-SR501 OUT
13const int KAPI_PIN = 33;                 // reed, diğer ucu GND / reed, other leg to GND
14const int TITRESIM_PIN = 32;             // SW-420 DO
15const int BUZZER_PIN = 25;               // aktif buzzer / active buzzer
16const int ROLE_PIN = 26;                 // siren rölesi / siren relay
17const int LED_PIN = 2;                   // dahili LED: kurulu / onboard LED: armed
18const bool ROLE_AKTIF_LOW = true;        // çoğu röle modülü LOW'da çeker / most relay modules switch on with LOW
19
20const unsigned long CIKIS_SURE = 30000;       // çıkış gecikmesi / exit delay
21const unsigned long GIRIS_SURE = 20000;       // giriş gecikmesi / entry delay
22const unsigned long SIREN_SURE = 180000;      // siren en fazla 3 dk / siren for at most 3 min
23const unsigned long PIR_ISINMA = 60000;       // PIR ısınma süresi / PIR warm-up time
24const unsigned long KONTROL_ARALIGI = 2000;   // Telegram'a 2 sn'de bir sor / poll Telegram every 2 s
25
26WiFiClientSecure client;
27UniversalTelegramBot bot(BOT_TOKEN, client);
28Preferences ayar;
29
30enum Durum { COZULU, CIKIS_GECIKMESI, KURULU, GIRIS_GECIKMESI, ALARM };
31const char* const DURUM_ADI[] = { "ÇÖZÜLÜ / DISARMED", "ÇIKIŞ GECİKMESİ / EXIT DELAY", "KURULU / ARMED", "GİRİŞ GECİKMESİ / ENTRY DELAY", "ALARM" };
32
33struct Giris {                           // gürültü süzgeçli giriş / debounced input
34  bool ham;
35  bool kararli;
36  unsigned long degisim;
37};
38Giris gKapi = {false, false, 0};
39Giris gPir = {false, false, 0};
40
41Durum durum = COZULU;
42unsigned long durumZamani = 0;
43bool evdeModu = false;
44bool sirenAcik = false;
45bool pirHazir = false;
46String tetikleyen = "";
47String bekleyen = "";                    // gönderilemeyen bildirimler / notifications not sent yet
48unsigned long sonDeneme = 0;
49unsigned long sonKontrol = 0;
50
51volatile unsigned long darbe[3] = {0, 0, 0};   // son 3 titreşim olayı / last 3 vibration events
52volatile bool zorlama = false;                 // 3 sn'de 3 darbe oldu / 3 hits within 3 s
53
54// Kesme: Telegram beklerken de kısa darbeler kaçmaz / interrupt: short pulses are caught even while Telegram blocks
55void IRAM_ATTR titresimKesmesi() {
56  unsigned long simdi = millis();
57  if (simdi - darbe[2] > 150) {          // 150 ms içindeki sekmeler tek olay / bounces within 150 ms count once
58    darbe[0] = darbe[1];
59    darbe[1] = darbe[2];
60    darbe[2] = simdi;
61    if (darbe[0] != 0 && simdi - darbe[0] <= 3000) zorlama = true;
62  }
63}
64
65bool suz(Giris &g, bool okuma, unsigned long sure) {
66  unsigned long simdi = millis();
67  if (okuma != g.ham) {
68    g.ham = okuma;
69    g.degisim = simdi;
70  }
71  if (simdi - g.degisim >= sure) g.kararli = g.ham;
72  return g.kararli;
73}
74
75void sirenYaz(bool acik) {
76  sirenAcik = acik;
77  if (ROLE_AKTIF_LOW) digitalWrite(ROLE_PIN, acik ? LOW : HIGH);
78  else                digitalWrite(ROLE_PIN, acik ? HIGH : LOW);
79}
80
81void bekleyeniGonder() {
82  sonDeneme = millis();
83  if (WiFi.status() != WL_CONNECTED) {
84    WiFi.reconnect();                    // bağlantı koptuysa yeniden dene / reconnect if the link dropped
85    return;
86  }
87  if (bot.sendMessage(CHAT_ID, bekleyen, "")) bekleyen = "";
88}
89
90void bildir(const String& metin) {       // gönderemezse 15 sn sonra yeniden dener / retries after 15 s if sending fails
91  Serial.println(metin);
92  bekleyen += metin + "\n";
93  if (bekleyen.length() > 1200) bekleyen = bekleyen.substring(bekleyen.length() - 1200);
94  bekleyeniGonder();
95}
96
97void durumDegis(Durum yeni) {
98  durum = yeni;
99  durumZamani = millis();
100  if (yeni == KURULU || yeni == COZULU) {   // yalnız kurma ve çözmede yazılır / written only on arm and disarm
101    ayar.putBool("kurulu", yeni == KURULU);
102    ayar.putBool("evde", evdeModu);
103  }
104}
105
106void alarmVer(const String& bolge) {
107  tetikleyen = bolge;
108  sirenYaz(true);                        // önce siren, sonra mesaj / siren first, message second
109  durumDegis(ALARM);
110  bildir("ALARM! " + bolge);
111}
112
113void kur(bool evde) {
114  if (gKapi.kararli) {
115    bildir("Kapı açık, kurulamadı / door open, not armed");
116    return;
117  }
118  evdeModu = evde;
119  for (int i = 0; i < 3; i++) darbe[i] = 0;
120  zorlama = false;
121  tetikleyen = "";
122  durumDegis(CIKIS_GECIKMESI);
123  bildir(evde ? "Evde modu, 30 sn sonra kurulu (PIR kapalı) / stay mode, armed in 30 s (PIR off)"
124              : "Tam kurulum, 30 sn içinde çık / away mode, leave within 30 s");
125}
126
127void coz() {
128  sirenYaz(false);
129  String m = "Alarm çözüldü / disarmed";
130  if (tetikleyen.length()) m += "\nAlarm hafızası / memory: " + tetikleyen;
131  tetikleyen = "";
132  durumDegis(COZULU);
133  bildir(m);
134}
135
136String durumMetni() {
137  String s = String("Durum / state: ") + DURUM_ADI[durum];
138  if (evdeModu && durum != COZULU) s += " (evde / stay)";
139  s += "\nKapı / door: ";
140  s += gKapi.kararli ? "AÇIK / open" : "kapalı / closed";
141  s += "\nWi-Fi: " + String(WiFi.RSSI()) + " dBm";
142  return s;
143}
144
145void mesajlariIsle(int adet) {
146  for (int i = 0; i < adet; i++) {
147    String chatId = bot.messages[i].chat_id;
148    String metin = bot.messages[i].text;
149    if (chatId != CHAT_ID) continue;     // yabancıya yanıt yok / strangers get no answer
150
151    int at = metin.indexOf('@');         // "/kur@bot_adi" -> "/kur"
152    if (at > 0) metin = metin.substring(0, at);
153
154    if (metin == "/kur" && durum == COZULU)        kur(false);
155    else if (metin == "/evde" && durum == COZULU)  kur(true);
156    else if (metin == "/coz" && durum != COZULU)   coz();
157    else if (metin == "/durum")                    bildir(durumMetni());
158    else bot.sendMessage(chatId, "/kur  /evde  /coz  /durum\n" + durumMetni(), "");
159  }
160}
161
162void setup() {
163  Serial.begin(115200);
164  pinMode(ROLE_PIN, OUTPUT);
165  sirenYaz(false);                       // açılışta siren kapalı / siren off at boot
166  pinMode(BUZZER_PIN, OUTPUT);
167  pinMode(LED_PIN, OUTPUT);
168  pinMode(PIR_PIN, INPUT);
169  pinMode(KAPI_PIN, INPUT_PULLUP);       // kapı kapalı = LOW, açık ya da kablo kesik = HIGH / closed = LOW, open or cut = HIGH
170  pinMode(TITRESIM_PIN, INPUT);
171  attachInterrupt(digitalPinToInterrupt(TITRESIM_PIN), titresimKesmesi, RISING);
172
173  ayar.begin("alarm", false);
174  if (ayar.getBool("kurulu", false)) {   // kesintiden önce kuruluysa kurulu başla / start armed if it was armed before the outage
175    evdeModu = ayar.getBool("evde", false);
176    durumDegis(KURULU);
177  }
178
179  WiFi.mode(WIFI_STA);
180  WiFi.begin(WIFI_SSID, WIFI_PASS);
181  unsigned long bas = millis();
182  while (WiFi.status() != WL_CONNECTED && millis() - bas < 20000) delay(500);   // Wi-Fi yoksa alarm yine çalışır / the alarm works without Wi-Fi too
183  client.setCACert(TELEGRAM_CERTIFICATE_ROOT);   // api.telegram.org kök sertifikası / root certificate of api.telegram.org
184  if (WiFi.status() == WL_CONNECTED) {
185    while (bot.getUpdates(bot.last_message_received + 1)) { }   // kapalıyken gelen eski komutları atla / skip commands sent while off
186  }
187  bildir(String("Sistem açıldı (elektrik geldi mi?) / system started (power restored?)\n") + durumMetni());
188}
189
190void loop() {
191  unsigned long simdi = millis();
192  if (!pirHazir && simdi >= PIR_ISINMA) pirHazir = true;
193  bool kapiAcik = suz(gKapi, digitalRead(KAPI_PIN) == HIGH, 50);
194  bool hareket = suz(gPir, digitalRead(PIR_PIN) == HIGH, 200) && pirHazir;
195  bool titresim = zorlama;
196  zorlama = false;
197
198  switch (durum) {
199    case CIKIS_GECIKMESI:
200      if (simdi - durumZamani >= CIKIS_SURE) {
201        durumDegis(KURULU);
202        bildir("Sistem kuruldu / system armed");
203      }
204      break;
205    case KURULU:
206      if (titresim)                  alarmVer("Pencere (titreşim) / window (vibration)");
207      else if (hareket && !evdeModu) alarmVer("İç mekân (PIR) / interior (PIR)");
208      else if (kapiAcik) {
209        durumDegis(GIRIS_GECIKMESI);
210        bildir("Kapı açıldı, 20 sn giriş süresi / door opened, 20 s entry delay");
211      }
212      break;
213    case GIRIS_GECIKMESI:
214      if (titresim)                                alarmVer("Pencere (titreşim) / window (vibration)");
215      else if (simdi - durumZamani >= GIRIS_SURE)  alarmVer("Giriş kapısı / entry door");
216      break;
217    case ALARM:
218      if (sirenAcik && simdi - durumZamani >= SIREN_SURE) {
219        sirenYaz(false);
220        bildir("Siren sustu, alarm hafızada / siren stopped, alarm kept in memory");
221      }
222      break;
223    default:
224      break;
225  }
226
227  unsigned long gecen = millis() - durumZamani;
228  bool buzzer = false;
229  if (durum == CIKIS_GECIKMESI)      buzzer = (gecen % 1000) < 60;
230  else if (durum == GIRIS_GECIKMESI) buzzer = (gecen % 400) < 200;
231  else if (durum == ALARM)           buzzer = sirenAcik;
232  digitalWrite(BUZZER_PIN, buzzer ? HIGH : LOW);
233  digitalWrite(LED_PIN, (durum == KURULU || durum == ALARM) ? HIGH : LOW);
234
235  if (millis() - sonKontrol > KONTROL_ARALIGI && WiFi.status() == WL_CONNECTED) {
236    int adet = bot.getUpdates(bot.last_message_received + 1);
237    while (adet) {
238      mesajlariIsle(adet);
239      adet = bot.getUpdates(bot.last_message_received + 1);
240    }
241    sonKontrol = millis();
242  }
243  if (bekleyen.length() && millis() - sonDeneme > 15000) bekleyeniGonder();
244}

ESP32のコードには、重要な違いが3つあります。1つ目に、Telegramへのメッセージ送信や新しいコマンドの確認には1〜2秒かかることがあり、その間loop()は待たされます。開いたままのドアや、少なくとも数秒間HIGHが続くPIRは影響を受けません。しかし、SW-420のミリ秒単位のパルスは取りこぼす恐れがあるので、振動は割り込み(attachInterrupt)で捉えます。2つ目に、alarmVer()(トルコ語で「アラームを出す」)はまずサイレンをオンにしてから、メッセージを送ります。インターネットがどれだけ遅くても、サイレンが遅れることはありません。送れなかった通知はキューに入れ、15秒ごとに再送を試みます。3つ目に、Preferencesライブラリで、警戒中かどうかをフラッシュメモリに保存します。警戒中に停電して復旧すると、ESP32は警戒状態で起動し、電源が戻ったことを知らせます。リレーとサイレンがESP32と同じ弱い電源を共有していると、Brownout detector was triggered(ブラウンアウト検出)のエラーでボードが再起動することがあります。対処法は「ESP32が再起動を繰り返すとき」の記事にあります。

Wi-Fiはいつも頼れるとは限りません。停電すればルーターも止まりますし、侵入者がルーターのプラグを抜くこともあります。ルーターを小型のUPS(無停電電源装置)につなぐか、予備の手段としてSIM800L GSMモジュールでSMSを送るようにすれば、どんな状況でも通知が届きます(日本ではGSMのサービスがないため、技適を取得したLTEモジュールが必要です)。動きを検知したときに写真も欲しい場合は、「ESP32-CAMでTelegramに写真を送る」の記事を参照してください。

Raspberry Pi PicoとMicroPythonでアラーム

同じステートマシンは、Raspberry Pi PicoのMicroPythonでも短いプログラムで作れます。キーパッドは、キーパッドのガイドにある行・列スキャンの関数で読み取ります。行はGP2〜GP5、列は内部プルアップを有効にしたGP6〜GP9につなぎます。PIRの電源はVBUS(5 V)から取ります。出力はもともと3.3 Vなので、GP16に直接つなげます。SW-420の電源は必ず3V3(OUT)ピンから取ってください。5 Vで動かしたモジュールの出力は、Picoの入力を壊す恐れがあります。振動のパルスはPin.irqで捉えて時刻をリストに記録し、状態はシンプルな文字列で管理します。時間の計算にはtime.ticks_diff()を使います。タイマーが一周して0に戻っても、正しい差を返してくれるからです。

MicroPython
1# Raspberry Pi Pico alarm: keypad + PIR + kapı + titreşim, durum makinesi / Pico alarm: keypad + PIR + door + vibration, state machine
2from machine import Pin
3import time
4
5SIFRE = "2580"                            # kendi şifreni yaz / set your own code
6CIKIS_MS, GIRIS_MS, SIREN_MS = 30000, 20000, 180000
7
8KEYS = ["123A", "456B", "789C", "*0#D"]
9rows = [Pin(n, Pin.OUT, value=1) for n in (2, 3, 4, 5)]       # R1–R4
10cols = [Pin(n, Pin.IN, Pin.PULL_UP) for n in (6, 7, 8, 9)]    # C1–C4
11pir = Pin(16, Pin.IN)                     # HC-SR501 OUT (3,3 V)
12kapi = Pin(15, Pin.IN, Pin.PULL_UP)       # reed, diğer ucu GND / reed, other leg to GND
13titresim = Pin(17, Pin.IN)                # SW-420 DO, modül 3V3'ten / module powered from 3V3
14buzzer = Pin(14, Pin.OUT, value=0)        # aktif buzzer / active buzzer
15role = Pin(13, Pin.OUT, value=1)          # aktif LOW röle: 1 = kapalı / active-LOW relay: 1 = off
16led = Pin("LED", Pin.OUT)                 # dahili LED: kurulu / onboard LED: armed
17
18darbeler = []                             # son titreşim zamanları / recent vibration times
19son_darbe = 0
20
21
22def titresim_irq(p):
23    global son_darbe
24    simdi = time.ticks_ms()
25    if time.ticks_diff(simdi, son_darbe) > 150:     # sekmeler tek olay / bounces count once
26        son_darbe = simdi
27        darbeler.append(simdi)
28
29
30titresim.irq(trigger=Pin.IRQ_RISING, handler=titresim_irq)
31
32
33def tus_oku():
34    for r, row in enumerate(rows):
35        row.value(0)                      # bu satırı etkinleştir / activate this row
36        for c, col in enumerate(cols):
37            if col.value() == 0:
38                row.value(1)
39                return KEYS[r][c]
40        row.value(1)
41    return None
42
43
44durum = "COZULU"
45zaman = time.ticks_ms()
46acilis = time.ticks_ms()
47pir_hazir = False
48evde = False
49girilen = ""
50son_tus = None
51
52
53def degis(yeni):
54    global durum, zaman
55    durum, zaman = yeni, time.ticks_ms()
56    print("Durum / state:", yeni)
57
58
59def alarm(bolge):
60    role.value(0)                         # siren açık / siren on
61    degis("ALARM")
62    print("ALARM! Bolge / zone:", bolge)
63
64
65while True:
66    tus = tus_oku()
67    if tus != son_tus:                    # değişimi 20 ms sonra doğrula / confirm the change after 20 ms
68        time.sleep_ms(20)
69        if tus_oku() != tus:
70            tus = son_tus                 # sekme, yok say / contact bounce, ignore it
71    if tus and tus != son_tus:            # her basış bir kez / each press once
72        if tus in "0123456789":
73            girilen = (girilen + tus)[-8:]
74        elif tus in "*C":
75            girilen = ""
76        else:
77            dogru = girilen == SIFRE
78            girilen = ""
79            if not dogru:
80                print("Yanlis sifre / wrong code")
81            elif durum == "COZULU" and tus in "AB":
82                if kapi.value():
83                    print("Kapi acik, kurulamaz / door open, cannot arm")
84                else:
85                    evde = tus == "B"     # B = evde modu, PIR kapalı / B = stay mode, PIR off
86                    darbeler.clear()
87                    degis("CIKIS")
88            elif durum != "COZULU" and tus == "#":
89                role.value(1)
90                degis("COZULU")
91    son_tus = tus
92
93    simdi = time.ticks_ms()
94    gecen = time.ticks_diff(simdi, zaman)
95    if not pir_hazir and time.ticks_diff(simdi, acilis) > 60000:
96        pir_hazir = True                  # PIR ısındı / PIR warmed up
97    kapi_acik = kapi.value() == 1
98    hareket = pir_hazir and pir.value() == 1 and not evde
99    darbeler[:] = [t for t in darbeler if time.ticks_diff(simdi, t) < 3000]
100    zorlama = len(darbeler) >= 3          # 3 sn'de 3 darbe / 3 hits in 3 s
101
102    if durum == "CIKIS" and gecen >= CIKIS_MS:
103        degis("KURULU")
104    elif durum == "KURULU":
105        if zorlama:
106            alarm("Pencere / window")
107        elif hareket:
108            alarm("PIR")
109        elif kapi_acik:
110            degis("GIRIS")
111    elif durum == "GIRIS":
112        if zorlama:
113            alarm("Pencere / window")
114        elif gecen >= GIRIS_MS:
115            alarm("Kapi / door")
116    elif durum == "ALARM" and role.value() == 0 and gecen >= SIREN_MS:
117        role.value(1)                     # siren susar, hafıza kalır / siren stops, memory stays
118
119    gecen = time.ticks_diff(time.ticks_ms(), zaman)
120    if durum == "CIKIS":
121        buzzer.value(1 if gecen % 1000 < 60 else 0)
122    elif durum == "GIRIS":
123        buzzer.value(1 if gecen % 400 < 200 else 0)
124    else:
125        buzzer.value(1 if durum == "ALARM" and role.value() == 0 else 0)
126    led.value(1 if durum in ("KURULU", "ALARM") else 0)
127    time.sleep_ms(10)

誤報を減らす方法

アラームシステムの最大の敵は、侵入者ではなく誤報です。無駄に3回鳴ったサイレンは、本人も近所の人も本気にしなくなります。次の対策の一部はすでにコードに入っています。残りは取り付けと配線に関するものです。

  • ウォームアップを待つ:PIRは電源投入後30〜60秒の間に勝手に作動することがあります。コードでは最初の60秒間を無視しています。
  • フィルタを使う:各入力が一定時間安定するのを待つこと(チャタリング対策)で、ケーブルに乗る短いノイズを取り除けます。
  • 衝撃の回数を数える:1回の衝撃ではなく3秒間に3回の衝撃を条件にすると、風、通り過ぎるトラック、ドアが勢いよく閉まる音などを除外できます。必要なら回数を4〜5回に増やすか、基板の半固定抵抗で感度を下げてください。
  • PIRの設置場所に気をつける:暖房器具、エアコン、日の当たる窓、カーテンに向けないでください。動きが視野を横切るときに、最もよく検知できます。ペットがいる場合は、センサーを高い位置に取り付けて少し下向きにし、感度を下げます。
  • 2つのセンサーを条件にする:重要な場所では、PIRとレーザー、またはPIRとドアが数秒以内に続けて作動することを条件にします(クロスゾーン)。1つのセンサーの故障がアラームにつながるのを防げます。
  • 電源をきれいにする:リレーがオンになったときの電源電圧の落ち込みで、PIRが作動することがあります。各センサーのVCCとGNDの間に100 µFと100 nFのコンデンサを入れ、リレーとサイレンの配線はセンサーの配線から離して引き回してください。
  • 長いケーブルには外付けのプルアップ抵抗を使う:Arduinoの内部プルアップは20〜50 kΩで、10メートルのケーブルではノイズを拾いやすくなります。4.7 kΩの外付け抵抗と、ピンに入れる100 nFのコンデンサで、ラインがずっと安定します。
  • レーザーを周囲の光から守る:LDRを黒い筒の中に入れ、しきい値を昼間と夜間でそれぞれテストしてください。
本格的に使い始める前に、ウォークテストを行ってください。サイレンをリレーのCOM端子から外し、警戒をセットして、侵入者が通りそうなルートを家の中で歩きます。どのゾーンがいつ作動したかをシリアルモニタで確認します。そのあと数日間ブザーだけで動かし、勝手に作動しないかを確かめます。

よくある間違いと対処法

  • 警戒をセットした直後にアラームが鳴る:PIRがまだウォームアップ中か、ドアの接点の配線が間違っています。センサーテストでは、ドアを閉めた状態でLOWになるはずです。HIGHになる場合は、磁石が離れすぎているか、どこかの線が切れています。
  • キーパッドが反応しない、または違うキーを読む:行と列の線が入れ替わっています。配線をやり直す代わりに、コードのsatirPinleri(行のピン)とsutunPinleri(列のピン)の配列の順番を変えてください。
  • 警戒をセットしようとすると長いビープ音が鳴る:これは故障ではなく、準備未完了の警告です。ドアが開いているか、レーザーのビームがLDRに届いていません。Dキーで各ゾーンの状態を確認してください。
  • 起動時にリレーが一瞬カチッと入る:ピンを出力に設定する前にオフのレベルを書き込んでいないか、アクティブHIGHのリレーなのにROLE_AKTIF_LOWをtrueのままにしています。
  • サイレンが鳴るとArduinoがリセットされる:サイレンとArduinoが同じ弱いACアダプターを共有していて、電圧降下でボードが再起動しています。より容量の大きいアダプター、別のコンバーター、太い線を使ってください。詳しくは「Arduinoがリセットを繰り返すとき」にあります。
  • 振動センサーがまったく作動しない、または作動しっぱなし:基板の半固定抵抗をゆっくり回してしきい値を調整し、センサーをガラスや窓枠にしっかり貼り付けてください。ぐらついているセンサーには衝撃が伝わりません。
  • ESP32がメッセージを送らない:トークンかチャットIDが間違っている、ネットワークが5 GHz、またはTelegramでボットを一度も開始していない、のいずれかです。ボットに一度/startを送らないと、ボットからメッセージを送ることはできません。
  • レーザーが昼間は問題ないのに夜に誤報を出す(またはその逆):しきい値が周囲の光のレベルに近すぎます。両方の環境でビームありとビームなしの値を測り、その中間で新しいLAZER_ESIKを選んでください。

アラームシステムを拡張するアイデア

  • カードで警戒・解除:キーパッドの横にRFID RC522リーダーを追加し、キーホルダー型のタグで解除します。
  • 24時間ゾーン:MQ-2ガスセンサーや炎センサーを追加して、警戒解除中でもアラームを出すガス・火災ゾーンを作ります。
  • ワイヤレスのドアセンサー:433 MHz RFモジュールを使って、離れた窓用の電池式ドアセンサーを作ります(日本では433 MHz帯をこの用途で自由に使うことはできず、技適マークのない無線モジュールの使用は電波法で制限されています)。
  • ディスプレイ付きの制御盤:I2C LCDにカウントダウン、状態、アラーム履歴を表示します。
  • スマートホームとの連携:ESP32をMQTTとHome Assistantにつなぎ、照明やカメラと一緒にアラームを管理します。
  • タンパー接点:ケースのふたにマイクロスイッチを付け、警戒解除中でもケースが開けられたらブザーが鳴るようにします。
  • 模型の家のプロジェクト:これらのゾーンをすべて模型の家にまとめ、スマートホームプロジェクトの防犯の層にします。

Arduinoのアラームシステムは、センサーの読み取り、タイミング、ステートマシン、電源設計を1つのプロジェクトで組み合わせるので、学習にもとても役立ちます。まずセンサーテストを動かします。次に数日間ブザーだけでアラームを試し、サイレンは最後につなぎましょう。BlueGraysアプリには、「リードスイッチ式ドアセンサー」「レーザー警報システム」「4x4キーパッドとLCDのパスワードロック」「Telegramで通知するESP32動体検知アラーム」などのプロジェクトがあり、ステップごとの回路図とコードを確認できます。

よくある質問

Arduinoでアラームシステムを作るにはどうすればいいですか?

Arduino Unoに、PIR人感センサー、ドア用のリード接点、キーパッド、アクティブブザー、サイレンを動かすリレーモジュールをつなぎます。コードはステートマシンとして書き、解除、退出遅延、警戒、入室遅延、アラームの各状態の間でシステムを切り替えます。警戒と解除はキーパッドの暗証番号で行い、警戒中にセンサーが作動するとサイレンが鳴ります。

Arduinoのアラームシステムにはどんなセンサーを使いますか?

よく使われるのは、室内用のPIR人感センサー(HC-SR501)、ドアや窓用のリード接点(MC-38)、ガラスへの衝撃を検知するSW-420振動センサー、廊下用のレーザー侵入警報です。ガス用にMQ-2、火災用に炎センサーを追加することもできます。各センサーはそれぞれ独立したゾーンとして設定し、ゾーンごとに動作を決めます。

停電してもArduinoのアラームは動きますか?

ACアダプターだけで動かしている場合は止まります。12 V 7 Ahのバッテリーを充電する13.8 Vのアラーム用電源を使えば、約100 mAを消費するUnoのシステムは4〜5日間動きます。ESP32版では警戒状態をフラッシュに保存するので、電源が戻るとシステムは警戒状態で復帰します。通知を送るには、ルーターもUPSにつないでおく必要があります。

Arduinoのアラームからスマホに通知を送れますか?

Arduino Uno単体ではできません。ESP32ならWi-Fi経由でTelegramにすぐメッセージを送れますし、スマホから/kur(警戒)や/coz(解除)などのコマンドも受け取れます。インターネットがない場所では、SIM800L GSMモジュールでSMSを送ったり電話をかけたりできます(日本ではGSMのサービスがないため、技適を取得したLTEモジュールが必要です)。両方の方法を組み合わせるのが最も確実です。

PIRセンサーが誤報を出すのはなぜですか?

よくある原因は、電源投入後30〜60秒のウォームアップ、暖房器具やエアコンなどの熱源、日差しで温まって揺れるカーテン、リレーがオンになったときの電源ノイズです。コードでウォームアップ時間を無視し、センサーを熱源から離し、VCCとGNDの間にコンデンサを入れれば、ほとんどの問題は解決します。

Arduinoのアラームシステムは実際の家で使えますか?

学習用や、部屋、ガレージ、物置の見守りには十分使えます。ただし、認証を受けたアラームシステムの代わりにはなりません。市販のシステムは、EN 50131などの規格に沿って、タンパー対策、予備電源、暗号化された無線通信、監視センターへの接続が試験されています。Arduinoのシステムを家で使う場合は、予備のバッテリー、タンパー接点、2つ目の通知手段を追加することをおすすめします。

全プロジェクトを見る
ESP32動体検知アラーム(PIR + ブザー + LED + Telegram)
#145上級
プレミアム

ESP32動体検知アラーム(PIR + ブザー + LED + Telegram)

PIRセンサーが動きを検知するとブザーとLEDを作動させ、同時にTelegramで持ち主に即座に通知するスマート防犯アラームです。アラームが繰り返される頻度を抑えるため、30秒のクールダウン(待ち時間)を設けています。この仕組みはPIRの検知が正しいかを確認したり、誤検知をなくしたりするものではありません。家の防犯、ガレージの監視、倉庫や作業部屋などに使えます。ESP32-CAMなら写真付きの通知を追加でき、通常のESP32 DevKit V1ではテキストで通知します。

0.0(0)
9
レーザー防犯システム
#23上級
プレミアム

レーザー防犯システム

映画のレーザーを使った強盗シーンのように、光センサーにレーザー光を当て、人が間に入って光を遮ると、赤いLEDが点灯してブザーが鳴り始めます。警報は自動では止まらず、ボタンやリモコンの電源ボタンで停止します。異常がないときは緑のLEDが点灯します。ドアや引き出しに、目に見えない見張り役を置くユニークな方法です。

4.8(60)
77
レーザー警報システム
#22中級
プレミアム

レーザー警報システム

映画のような、目に見えない光のフェンスを作ります。向かい側の光センサーにレーザーを当て、人が通って光を遮るとすぐにLEDが点灯し、ブザーが鳴ります。光が再び届くと、LEDが消灯し、ブザーも止まります。ドアや廊下に使える、シンプルながら印象的な警報装置です。

4.8(64)
83
リードスイッチ式ドアセンサー
#38初級
プレミアム

リードスイッチ式ドアセンサー

ライトを見るだけで、ドアが開いているか閉まっているか分かります。ドアに小さな磁石、ドア枠に磁石を検知するスイッチを取り付けます。ドアが閉まっているときは両者が並び、緑のLEDが点灯します。ドアが開いて磁石が離れると、赤いLEDが点灯します。家庭用警報装置のドアセンサーも、こんなシンプルな仕組みです。

4.7(34)
44
4x4キーパッドとLCDで暗証番号ロック
#64上級
プレミアム

4x4キーパッドとLCDで暗証番号ロック

正しい暗証番号を知らないと開けられない、小さな電子ドアロックです。ハッシュキーを押してキーパッドから暗証番号を入力します。正しければ画面に入室許可が表示され、緑のLEDが点灯し、サーボモーターがドアを5秒間開けてから再び閉めます。間違っていれば赤のLEDが点灯します。建物や金庫のロックの仕組みを自分で作ります。

4.8(59)
75
人感センサーで動きを検出
#24初級
プレミアム

人感センサーで動きを検出

誰かが部屋に入ると知らせる、シンプルな見張り役です。人感センサーが人体から放出される熱を捉え、Arduinoは動きがある間LEDを点灯させ、動きが止まると消灯させます。玄関先で自動点灯する照明や、警報システムの基本となる仕組みです。

4.6(47)
67
スマート電子金庫
#92上級
プレミアム

スマート電子金庫

貴重品を保管できる、暗証番号で開く小さな金庫です。キーパッドで4桁の暗証番号を入力すると、正しければサーボモーターが解錠し、緑色のLEDが点灯してディスプレイに金庫の状態が表示されます。キーを押すたびにブザーが短く鳴ります。3回間違えると30秒間ロックアウトされます。暗証番号はいつでも変更でき、電源が切れても保持されます。

4.7(42)
63