Arduinoでつくるアラームシステム:PIR、ドアセンサー、キーパッド、サイレンのガイド
PIR、リード接点のドアセンサー、振動とレーザーのゾーン、キーパッドの暗証番号、入室・退出遅延、サイレン用リレー、ESP32のTelegram通知を使って、Arduinoのアラームシステムを作ります。
Arduinoのアラームシステムは、自分で作れる防犯装置です。家や部屋に誰かが無断で入るとサイレンを鳴らします。ドアが開いたときは、暗証番号を入力するための数秒間の猶予があります。必要ならスマホにすぐ通知を送ることもできます。このガイドでは、PIR人感センサー、ドアに取り付けるリード接点、窓ガラスへの衝撃を捉える振動センサー、廊下を横切るレーザー侵入警報を使って、4ゾーンのアラームを作ります。警戒と解除は4x4キーパッドの暗証番号で行い、12 Vのサイレンはリレー経由で鳴らします。これらをすべて1台のArduino Unoで動かします。
市販のアラーム制御盤が行っている機能を、小さな形でステップごとに再現していきます。入室・退出遅延、在宅モード(夜間モード)、暗証番号の誤入力によるロックアウト、サイレンの鳴動時間の上限、アラーム履歴です。コードは入れ子のifブロックではなくステートマシンとして書きます。そのため、アラームがその瞬間に何をしているのかが常にはっきりします。そのあと同じロジックをESP32に移し、Telegramでの警戒・解除とアラーム通知を追加します。さらにRaspberry Pi Pico向けのMicroPython版を書き、いちばん厄介な問題である誤報を減らす方法も紹介します。

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リレーモジュール | 1 | Arduinoから絶縁して12 Vのサイレンを切り替える |
| 12 Vのサイレン(できればストロボ付き) | 1 | メインのアラーム音。通常100〜120 dB |
| 赤と緑のLED+220 Ωの抵抗2本 | 1セット | 警戒中と準備完了の表示 |
| 12 V電源、LM2596モジュール、12 Vバッテリー(任意) | 1セット | 電源と停電時のバックアップ |
| ブレッドボード、ジャンパーワイヤー、4芯の防犯用ケーブル | ― | 配線と離れた場所のセンサー |







センサー:各ゾーンは何を検知するのか
- 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のサイレンが担当し、リレーモジュール経由で鳴らします。アクティブブザーとパッシブブザーの違いは「ブザーとは?」を参照してください。
配線図とピン表
次の表は、3種類のボードの配線をまとめたものです。Arduino Uno版は、4つのゾーンとキーパッドをすべて使います。ESP32版はキーパッドの代わりにTelegramのコマンドで警戒・解除し、Pico版は短くまとめているため、レーザー侵入警報はUno版だけにあります。手元のボードのピン番号は「ピン配置」ツールで、LEDの抵抗値はLED抵抗計算ツールで確認できます。
| 接続 | Arduino Uno | ESP32 | Raspberry 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 / GND | 5V / D10 / GND | VIN (5V) / GPIO 27 / GND | VBUS / GP16 / GND |
| リード接点(2本の線) | D11とGND | GPIO 33とGND | GP15とGND |
| SW-420 VCC / DO / GND | 5V / D12 / GND | 3V3 / GPIO 32 / GND | 3V3(OUT) / GP17 / GND |
| KY-008 S / −(レーザーは常時点灯) | 5V / GND | ― | ― |
| LDRは5VとA0の間、10 kΩはA0とGNDの間 | A0 | ― | ― |
| アクティブブザーの+(−はGND) | A1 | GPIO 25 | GP14 |
| リレーモジュールのIN | A2 | GPIO 26 | GP13 |
| 緑のLED(220 Ω付き):準備完了 | A3 | ― | ― |
| 赤のLED(220 Ω付き):警戒中 | D13 | GPIO 2(内蔵LED) | 内蔵LED |
アラームがいちばん役に立つのは停電のときです。電源は最初から考えておきましょう。実用的な構成を紹介します。プロ用の制御盤と同じように、バッテリーを接続できる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の電源の取り方」を参照してください。
ステップごとの組み立て
- Arduino IDEでライブラリマネージャを開き、Keypadと入力して、Mark Stanley氏とAlexander Brevig氏のライブラリをインストールします。インストールしていないと、コンパイル時にKeypad.h: No such file or directory(ファイルまたはディレクトリがない)というエラーが出ます。
- キーパッドをキーが手前に見える向きで持ちます。左から最初の4本の線(R1〜R4)をD9, D8, D7, D6に、次の4本(C1〜C4)をD5, D4, D3, D2につなぎます。
- PIRを5V、D10、GNDにつなぎます。2つの半固定抵抗はどちらも反時計回りにいっぱいまで回し、トリガー設定のジャンパーをHにします。
- リード接点の2本の線をD11とGNDに、SW-420を5V、D12、GNDにつなぎます。
- LDRを5VとA0の間に、10 kΩの抵抗をA0とGNDの間に挿します。LDRは黒い筒の奥に入れ、レーザーが筒のちょうど中心に当たるように光軸を合わせます。
- ブザーをA1に、リレーモジュールのINをA2につなぎ、VCCとGNDはArduinoの5VとGNDにつなぎます。LEDは220 Ωの抵抗を通してA3とD13につなぎます。
- まず下のセンサーテストを書き込み、シリアルモニタを9600ボーで開いて、センサーを1つずつ作動させます。
- 次にアラームのスケッチ全体を書き込み、暗証番号を変更します。サイレンをつなぐ前に、数日間ブザーだけでシステムを試してください。
最初のテスト:すべてのセンサーをシリアルモニタで確認する
アラームのコードに進む前に、各センサーが思いどおりに動くか確認しましょう。この小さなスケッチは、0.5秒ごとに4つの情報を表示します。PIRの状態、ドアが開いているか閉じているか、直近0.5秒間の振動パルスの数、LDRのアナログ値です。振動のピンは常に読み続けています。SW-420のパルスは数ミリ秒しか続かないことがあり、途中でdelay()を使うコードでは取りこぼしてしまうからです。
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}ステートマシン:アラームの頭脳
アラームを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リーダーを追加する余裕は十分にあります。
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にだけ応答し、ほかの人からのコマンドは黙って無視します。
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が再起動を繰り返すとき」の記事にあります。
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に戻っても、正しい差を返してくれるからです。
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を黒い筒の中に入れ、しきい値を昼間と夜間でそれぞれテストしてください。
よくある間違いと対処法
- 警戒をセットした直後にアラームが鳴る: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)
PIRセンサーが動きを検知するとブザーとLEDを作動させ、同時にTelegramで持ち主に即座に通知するスマート防犯アラームです。アラームが繰り返される頻度を抑えるため、30秒のクールダウン(待ち時間)を設けています。この仕組みはPIRの検知が正しいかを確認したり、誤検知をなくしたりするものではありません。家の防犯、ガレージの監視、倉庫や作業部屋などに使えます。ESP32-CAMなら写真付きの通知を追加でき、通常のESP32 DevKit V1ではテキストで通知します。

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

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

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

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

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

スマート電子金庫
貴重品を保管できる、暗証番号で開く小さな金庫です。キーパッドで4桁の暗証番号を入力すると、正しければサーボモーターが解錠し、緑色のLEDが点灯してディスプレイに金庫の状態が表示されます。キーを押すたびにブザーが短く鳴ります。3回間違えると30秒間ロックアウトされます。暗証番号はいつでも変更でき、電源が切れても保持されます。
ほかの記事
BlueGraysアプリ
この回路をスマホでステップごとに作る
回路図、コード、部品リストを1つの画面で確認できます。Arduino、ESP32、Picoのプロジェクトを200件以上用意しています。

