KY-008レーザーモジュールとArduino:レーザー侵入警報とタイマー
KY-008レーザーモジュールとArduinoの使い方を解説します。配線、5 mW・650 nmレーザーの目の安全、LDRを使ったレーザー侵入警報、しきい値の自動キャリブレーション、鏡、レースタイマー、ESP32での通知まで扱います。
レーザーモジュールは、Arduinoで使える部品の中でもとりわけ目を引く部品です。細い赤いビームを出し、そのビームは広がらずに何メートルも届きます。電子工作キットに入っているKY-008レーザーモジュールは、約5 mWの650 nmレーザーダイオードを小さな基板に載せたものです。Arduinoのデジタルピン1本につなぎ、digitalWrite()でオン・オフできます。単体ではただのポインターですが、向かい側に光センサーを置けば、映画のようなレーザー侵入警報にも、ドアを通る人を数えるカウンターにも、ミリ秒単位で正確なレースタイマーにもなります。
このガイドでは、KY-008の仕様、5 mWのレーザーが目にとって実際どれほど危険で、どんなルールを守るべきか、そしてArduino Uno、ESP32、Raspberry Pi Picoへの正しい配線を学びます。そのあとLDRでレーザーバリアを作り、しきい値を当て推量ではなくArduinoに自動で測らせ、鏡で1つのレーザーを複数のビームにし、通過を数えて速度を測り、ビームが遮られたらESP32でスマホに通知を送ります。

KY-008レーザーモジュールとは?その仕組みは?
モジュールの心臓部は、真ちゅうの筒の中に取り付けられた小さなレーザーダイオードです。普通のLEDと違い、レーザーダイオードは電流があるしきい値を超えると、単一の波長の光をそろった状態(コヒーレント)で出します。筒の先にある小さなレンズが、この光をほぼ平行なビームにします。そのため、LEDの光は数センチで広がってしまいますが、レーザーの点は数メートル先でも直径数ミリにとどまります。650 nmは目によく見える深い赤です。レーザーヘッドの後ろの小さな基板に載った抵抗が、ダイオードに流れる電流を制限しています。レーザーダイオードはLEDよりもはるかに過電流に弱く、この抵抗を飛ばしてダイオードに直接電源をつなぐと、数秒で壊れることがあります。
| 項目 | 値 |
|---|---|
| 型番 | KY-008(HW-493として売られていることもある) |
| 波長 | 650 nm(可視光の赤) |
| 出力 | 約5 mW |
| レーザーのクラス | 3R(IEC 60825-1、日本ではJIS C 6802)、米国ではIIIa |
| 動作電圧 | 5 V(3.3 Vでは暗くなる) |
| 電流 | 40 mA未満、基板によって異なる |
| レーザーヘッド | 6 mmの真ちゅうの筒 |
| ピン | S(信号)、中央のピン(ほとんどの基板で+5Vまたは未接続)、−(GND) |
| 動作温度 | −10〜40 °C |
目の安全:5 mWのレーザーはどれくらい危険?
レーザーの危険は、全体の出力そのものではなく、その出力がごく小さな点に集中することから生まれます。目のレンズ(水晶体)は平行なビームを網膜上のミクロン単位の点に集めるので、壁に当たると無害に見える5 mWでも、網膜上では太陽を直接見るよりも強い点になりえます。レーザーはこの危険度に応じてクラスに分けられています。1 mW未満のクラス2のレーザーでは、まばたき反射(約0.25秒)でたいてい十分に目が守られます。KY-008が属するクラス3Rはその上限の5倍までの出力です。うっかり一瞬見てしまう程度なら危険は低いですが、ビームを直接または鏡越しにのぞき込むと、目を傷めることがあります。
| クラス | 出力(可視光) | 危険度 |
|---|---|---|
| 1 | とても低い、またはビームが機器の内部に閉じ込められている | 通常の使用では安全 |
| 2 | 最大1 mW | まばたき反射で守られる。わざと見つめない |
| 3R | 1〜5 mW(KY-008) | 直接見ると目を傷めることがある |
| 3B | 5〜500 mW | 直接のビームも鏡の反射光も危険 |
| 4 | 500 mW超 | 目、皮膚、火災の危険 |
- ビームを絶対にのぞき込まない:レーザーの出射口を目に近づけないでください。点灯したままモジュールを手に持って振り回すのもやめましょう。
- ビームは目の高さより下に:警報やカウンターのビームは床から20〜50 cmの高さに取り付けます。この高さは、はいはいする赤ちゃんや小さな子ども、猫や犬の目の高さと同じなので、そばにいるときはレーザーを点けたままにしないでください。
- 反射に注意:鏡、ガラス、腕時計の文字盤、光沢のある金属はビームを思わぬ方向に送ります。ビームの経路を最初から最後まで確認し、つや消しの暗い面で終わらせてください。
- 子どもだけで使わない:レーザーはおもちゃではありません。学校や科学コンテストのプロジェクトでは、大人の監督のもとで作業してください。
- 表示をうのみにしない:安いモジュールの実際の出力は、表示と違うことがあります。どのモジュールも少なくともクラス3Rとして扱ってください。
- 紙を使って光軸を合わせる:ビームの前に白い紙をかざし、紙の上の点を追ってください。ビームの行き先を確かめるためにビームをのぞき込むことは絶対にしないでください。
KY-008の配線:Arduino Uno、ESP32、Raspberry Pi Pico
KY-008には3本のピンがあります。Sは信号ピンでHIGHにするとレーザーが点灯し、−はGND、印のない中央のピンはほとんどの基板で+5Vにつながっているか、どこにもつながっていません。レーザーはSと−のあいだに入っているので、SにつないだArduinoのピンがレーザーの電流を直接供給します。Arduino Unoなら短いテストには問題ありませんが、モジュールの電流は基板によって違い、40 mA近くになることがあります。これはATmega328Pの1ピンあたりの絶対最大定格で、推奨値は20 mAです。レーザーを何時間も点けたままにする場合、複数のレーザーを使う場合、ESP32やPicoで使う場合は、レーザーを5Vから給電し、NPNトランジスタ(BC547、S8050、2N2222)でスイッチングしてください。トランジスタのピン配置は「トランジスタのピン配置」ツールで確認できます。
| 接続 | Arduino Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|
| KY-008のS | D7 | 5V(VIN) | VBUS(40番ピン) |
| KY-008の中央のピン | 未接続(レーザーが点かなければ5V) | 5V(VIN) | VBUS(40番ピン) |
| KY-008の− | GND | NPNのコレクタ | NPNのコレクタ |
| NPNのエミッタ | ― | GND | GND(38番ピン) |
| NPNのベース(1 kΩ経由) | ― | GPIO 26 | GP15(20番ピン) |
| LDRの足1 | 5V | 3V3 | 3V3(OUT)(36番ピン) |
| LDRの足2+抵抗の上側 | A0 | GPIO 34 | GP26(31番ピン) |
| 抵抗の下側(3.3〜10 kΩ) | GND | GND | GND |
| アクティブブザーの+ | D8 | GPIO 25 | GP14(19番ピン) |
スケッチに進む前に、次の小さなテストプログラムでレーザーとLDRを一緒に試してみましょう。0.5秒ごとにレーザーをオン・オフし、両方の状態の読み取り値をシリアルモニタに表示します。2つの値の差が大きいほど、バリアの信頼性が高くなります。Unoなら差が300を超えればとても良好です。100未満なら、ビームがLDRにまっすぐ当たっていないか、周囲の光が強すぎます。
1// KY-008 + LDR ilk test: lazer açıkken ve kapalıyken değerleri gör / First test: see readings with the laser on and off
2const int LASER_PIN = 7; // KY-008 S pini / KY-008 S pin
3const int LDR_PIN = A0; // LDR bölücü çıkışı / LDR divider output
4
5void setup() {
6 Serial.begin(9600);
7 pinMode(LASER_PIN, OUTPUT);
8}
9
10void loop() {
11 digitalWrite(LASER_PIN, HIGH); // lazer açık / laser on
12 delay(500); // LDR'nin oturmasını bekle / let the LDR settle
13 int onValue = analogRead(LDR_PIN);
14
15 digitalWrite(LASER_PIN, LOW); // lazer kapalı / laser off
16 delay(500);
17 int offValue = analogRead(LDR_PIN);
18
19 Serial.print(F("Isin var / beam: "));
20 Serial.print(onValue);
21 Serial.print(F(" Isin yok / no beam: "));
22 Serial.print(offValue);
23 Serial.print(F(" Fark / gap: "));
24 Serial.println(onValue - offValue);
25}LDRを使ったレーザーバリアの仕組みは?
レーザーバリアの考え方はシンプルです。レーザーのビームが、反対側にあるLDR(光によって抵抗値が変わる抵抗)に当たり続けます。レーザーの点が当たっているとLDRの抵抗値は数百オームまで下がり、ビームが遮られると数十キロオームまで上がります。LDRを固定抵抗と組み合わせて分圧回路にすると、この抵抗値の変化がアナログピンの電圧の変化になります。5V → LDR → A0 → 抵抗 → GNDの配線では、ビームが当たっているあいだA0の値は高く、遮られると低くなります。誰かがビームを横切った瞬間に値が下がり、Arduinoが警報を鳴らします。



簡単な計算で、レーザーがLDRにこれほど強く効く理由がわかります。5 mWのビームが直径約3 mmの点に集まると、強さは1平方メートルあたりおよそ700 Wになります。これは真昼の日光と同じくらいで、明るく照らした部屋の数百倍です。それでも、バリアの最大の敵は周囲の光です。日の当たる窓の前では、ビームが遮られてもLDRは明るいままで、警報は何も気づきません。対策はとても安上がりです。LDRを長さ3〜5 cmの黒い筒(空のボールペンの軸、黒いストロー、熱収縮チューブ)の奥に入れます。筒は正面から来るレーザーだけを通し、横からの周囲の光をさえぎります。
| 状態 | LDRの抵抗値(例) | 10 kΩでのanalogRead | 3.3 kΩでのanalogRead |
|---|---|---|---|
| レーザーがLDRに当たっている(筒の中) | 300 Ω | 993 | 938 |
| ビームが遮られている(筒の中) | 30 kΩ | 256 | 101 |
| ビームが遮られている(筒なし、明るい部屋) | 3 kΩ | 787 | 536 |
| ビームありとビームなしの差(筒の中) | ― | 737 | 837 |
ステップごとに:Arduinoでレーザー侵入警報を作る
- KY-008のSピンをD7に、−ピンをGNDにつなぎます。中央のピンは未接続のままにします(レーザーが点かなければ5Vにつなぎます)。
- LDRの片方の足を5Vに、もう片方をA0につなぎ、A0とGNDのあいだに3.3 kΩまたは10 kΩの抵抗を入れます。
- LDRを黒い筒の奥に入れ、筒の口をレーザーに向けます。
- アクティブブザーの+の足をD8に、−の足をGNDにつなぎます。消音ボタンはD4とGNDのあいだに取り付けます。内部プルアップを使うので、追加の抵抗は不要です。
- レーザーとLDRをしっかりした土台に固定します。まず白い紙でレーザーの点を見つけ、それから筒の中に導きます。
- スケッチを書き込み、シリアルモニタを9600ボーで開きます。起動時にはビームの前に誰もいないようにしてください。Arduinoはレーザーを一度オフにしてからオンにして、自分でキャリブレーションします。
1// KY-008 lazer + LDR ile lazerli alarm / Laser tripwire alarm with KY-008 + LDR
2const int LASER_PIN = 7; // KY-008 S pini ya da transistör bazı / KY-008 S pin or transistor base
3const int LDR_PIN = A0; // LDR bölücü çıkışı / LDR divider output
4const int BUZZER_PIN = 8; // aktif buzzer / active buzzer
5const int LED_PIN = 13; // kart üzerindeki LED / on-board LED
6const int RESET_PIN = 4; // susturma butonu, diğer ucu GND / silence button, other leg to GND
7
8const int BREAK_PCT = 40; // %40'ın altı = ışın kesik / below 40% = beam broken
9const int RESTORE_PCT = 60; // %60'ın üstü = ışın sağlam / above 60% = beam intact
10const unsigned long CONFIRM_MS = 30; // en az bu kadar kesik kalmalı / must stay broken this long
11const unsigned long ALARM_MS = 10000; // alarm süresi / alarm duration
12
13int onLevel = 0, offLevel = 0; // ışınlı ve ışınsız ölçüm / reading with and without the beam
14bool beamOk = true;
15bool alarmOn = false;
16unsigned long brokenSince = 0;
17unsigned long alarmStart = 0;
18
19int readAvg() {
20 long sum = 0;
21 for (int i = 0; i < 8; i++) sum += analogRead(LDR_PIN); // 8 ölçüm ortalaması / average of 8 readings
22 return sum / 8;
23}
24
25void calibrate() {
26 while (true) {
27 digitalWrite(LASER_PIN, LOW); // lazer kapalı = kesik ışın / laser off = broken beam
28 delay(300); // LDR yavaştır, bekle / the LDR is slow, wait
29 offLevel = readAvg();
30 digitalWrite(LASER_PIN, HIGH); // lazer açık = sağlam ışın / laser on = intact beam
31 delay(300);
32 onLevel = readAvg();
33 Serial.print(F("Isinli / beam: "));
34 Serial.print(onLevel);
35 Serial.print(F(" Isinsiz / no beam: "));
36 Serial.println(offLevel);
37 if (abs(onLevel - offLevel) >= 100) return; // yeterli fark var / enough difference
38 Serial.println(F("Isin LDR'ye ulasmiyor, hizala! / Beam misses the LDR, align it!"));
39 delay(2000);
40 }
41}
42
43// 100 = ışın tam, 0 = ışın yok; LDR'nin yönü fark etmez / 100 = full beam, 0 = no beam; LDR polarity doesn't matter
44int beamPercent() {
45 return (long)(readAvg() - offLevel) * 100 / (onLevel - offLevel);
46}
47
48void setup() {
49 Serial.begin(9600);
50 pinMode(LASER_PIN, OUTPUT);
51 pinMode(BUZZER_PIN, OUTPUT);
52 pinMode(LED_PIN, OUTPUT);
53 pinMode(RESET_PIN, INPUT_PULLUP);
54 calibrate();
55 Serial.println(F("Alarm hazir / Alarm armed"));
56}
57
58void loop() {
59 unsigned long now = millis();
60 int pct = beamPercent();
61
62 if (beamOk && pct < BREAK_PCT) { // ışın yeni kesildi / beam just broke
63 beamOk = false;
64 brokenSince = now;
65 } else if (!beamOk && pct > RESTORE_PCT) {
66 beamOk = true; // ışın geri geldi / beam restored
67 }
68
69 if (!beamOk && !alarmOn && now - brokenSince >= CONFIRM_MS) {
70 alarmOn = true;
71 alarmStart = now;
72 Serial.println(F("ALARM: isin kesildi! / beam broken!"));
73 }
74
75 if (alarmOn && (now - alarmStart >= ALARM_MS || digitalRead(RESET_PIN) == LOW)) {
76 alarmOn = false; // süre doldu ya da butona basıldı / timeout or button pressed
77 }
78
79 digitalWrite(LED_PIN, beamOk ? LOW : HIGH);
80 digitalWrite(BUZZER_PIN, (alarmOn && (now / 200) % 2) ? HIGH : LOW); // kesik siren / beeping siren
81}このスケッチは固定のしきい値を使いません。起動時にcalibrate()がレーザーをオフにして、ビームがないときのLDRの値を測り、次にオンにしてビームがあるときの値を測ります。beamPercent()は、この2つの値をもとに各読み取り値を0〜100のパーセントに変換します。100はビームが完全に届いている状態、0は完全に遮られている状態です。この計算は、LDRが分圧回路の上側にあっても下側にあっても関係ありません。光で値が上がる場合でも下がる場合でも、結果は同じです。ビームが40%を下回り、その状態が少なくとも30 ms続くと、警報が10秒間鳴り、D4のボタンで早めに止められます。ビームがまだ遮られていれば警報は再び鳴り始めるので、誰かがビームの中に立っているかぎりシステムは静かになりません。
しきい値のキャリブレーション:数値を直接書き込むべきでない理由
ネット上のレーザー警報の例の多くは、「analogRead(A0) < 500」のような固定のしきい値を使っています。机でテストしたその晩は動きますが、翌朝部屋に日が差し込んだとき、レーザーを数ミリ動かしてしまったとき、LDRを交換したときには失敗します。正しいしきい値は、ビームがあるときとないときの読み取り値のちょうど中間です。起動のたびに両方の値を測り直す自動キャリブレーションなら、システムが周囲の環境に合わせて調整されます。上のスケッチでは、しきい値は1つの数値ではなく2つです。40%を下回るとビームが遮られたとみなし、60%を上回るとビームが戻ったとみなします。この差をヒステリシスといいます。読み取り値がしきい値のすぐ近くで揺れているときに、警報が1秒に何十回もオン・オフするのを防ぎます。
| 設定 | コード内の値 | 役割 | 変えるべきとき |
|---|---|---|---|
| BREAK_PCT | 40 | ビームが遮られたとみなすレベル | 半透明の物体(ガラス、薄い布)も検出したいなら60〜70に上げる |
| RESTORE_PCT | 60 | ビームが戻ったとみなすレベル | 常にBREAK_PCTより15〜20ポイント高くしておく |
| CONFIRM_MS | 30 | 短い乱れ(虫、ほこり、振動)を無視する | 誤報が出るなら100〜200 msに上げる。速い通過を見逃すなら下げる |
| 平均する読み取り回数 | 8 | 電気的なノイズや照明のちらつきをならす | Unoでは1回の読み取りに約0.1 msかかる。8回でも1 ms未満 |
| キャリブレーションの差 | 100 | ビームがLDRに届かなければセットアップを止める | とても長い距離やとても明るい部屋では50まで下げてもよい |
長時間動かすシステムでは、周囲の光が1日のうちにゆっくり変わります。簡単に補正するには、ビームが当たっていて警報が鳴っていないときに、calibrate()を再び呼び出します(たとえば1時間ごと)。レーザーをオフにするのはArduino自身なので、この短い暗さを侵入者と間違えることはありません。読み取り値を見るには、Arduino IDEのシリアルプロッタがとても便利です。ビームの中で手を振ると、グラフがどこまで下がり、戻るまでにどれくらいかかるかがわかります。
鏡で複数のビーム:1つのレーザーで網を張る
ビームが1本だけだと、しゃがんでくぐったりまたいだりして簡単に通り抜けられます。映画で見るようなレーザーの網を作るには、ビームごとにレーザーとLDRを用意する代わりに、鏡で1つのレーザーをジグザグに反射させます。ドアや廊下の両側に鏡を向かい合わせに置くと、ビームが左右に反射を繰り返し、最後にLDRに届きます。どこか1か所でも遮られればLDRが暗くなるので、1つのLDRと同じスケッチで網全体を守れます。反射角は入射角と等しいので、それぞれの鏡をほんの少し傾けて、ビームを次の鏡の中心に送るだけです。
鏡はどれも光の一部を失います。普通の洗面所の鏡は約90%を反射し、前面のガラスでかすかな2つ目の点(ゴースト反射)ができます。反射層がガラスの表面にある表面鏡は95%以上を反射し、きれいな点が1つだけできます。損失は掛け算で重なるので、鏡を増やすとLDRに届く光はどんどん減っていきます。
| 鏡の枚数 | 普通の鏡(90%) | 表面鏡(95%) |
|---|---|---|
| 1 | 90% | 95% |
| 2 | 81% | 90% |
| 4 | 66% | 81% |
| 6 | 53% | 74% |
| 8 | 43% | 66% |
この表から大切なことがわかります。レーザーの点は周囲の光の数百倍明るいので、キャリブレーションするスケッチなら43%でも十分すぎるほどです。本当の難題は光の損失ではなく光軸合わせです。ビームの経路が長いほど、小さな角度のずれが大きくなります。1度のずれで、5 m先では点が約9 cmずれます。安いレーザーのビームは途中で少しずつ広がるので、数メートル先では点がLDRの5 mmの受光面より大きくなることもあります。
- レーザー、鏡、LDRの筒は、しっかりしたホルダー(ネジ止めのL字金具、3Dプリントしたホルダー、ホットボンドで固定した木のブロック)に取り付けます。手で持った鏡では、光軸は決して合ったままになりません。
- 部屋を暗くし、最初の鏡の前に白い紙をかざしてレーザーの点を見つけます。
- 最初から最後まで順番に鏡の向きを合わせます。点が次の鏡の中心に来るまで、それぞれの鏡をほんの少しずつ傾けます。
- 最後のビームをLDRの筒の口に向け、テスト用のスケッチを実行して、シリアルモニタの差がいちばん大きくなる位置を探します。
- すべての光軸が合ったら、ホットボンドか固定用のネジで鏡の角度を固定します。机を少しぶつけただけでも、光軸がずれることがあります。
- 網の各区間を1つずつ手で遮ってテストし、毎回警報が鳴ることを確認します。
通過カウンター:何人通った?どれくらいの時間?
同じバリアで、ドアを通る人、コンベヤーで運ばれる箱、ゴミ箱に落ちるボールを数えられます。コツは、1回の通過を1回だけ数えることです。次のスケッチはビームが遮られた瞬間を記録し、ビームが戻ったときに通過を数えるので、しばらくビームの中に立っている人も1回の通過として数えられます。15 msより短い遮断は、ほこりや電気的なノイズとみなして無視します。スケッチはビームが遮られていた時間も表示します。物体の長さがわかっていれば、この時間からそのまま速度が求められます(v = L / t)。たとえば25 cmのおもちゃの車が50 msビームを遮ったなら、速度は0.25 / 0.05 = 5 m/sです。
1// Lazer bariyeriyle geçiş sayacı ve kesilme süresi / Laser barrier pass counter and block time
2const int LASER_PIN = 7; // KY-008 S pini / KY-008 S pin
3const int LDR_PIN = A0; // LDR bölücü çıkışı / LDR divider output
4const unsigned long MIN_BREAK_MS = 15; // daha kısa kesintiler parazittir / shorter breaks are noise
5const float OBJECT_LENGTH_M = 0.0; // cismin boyu biliniyorsa yaz (ör. 0.25) / object length if known (e.g. 0.25)
6
7int onLevel = 0, offLevel = 0;
8bool beamOk = true;
9unsigned long brokenSince = 0;
10unsigned long passCount = 0;
11
12int readAvg() {
13 long sum = 0;
14 for (int i = 0; i < 4; i++) sum += analogRead(LDR_PIN); // 4 ölçüm yeterli ve hızlı / 4 readings: enough and fast
15 return sum / 4;
16}
17
18void calibrate() {
19 while (true) {
20 digitalWrite(LASER_PIN, LOW);
21 delay(300);
22 offLevel = readAvg();
23 digitalWrite(LASER_PIN, HIGH);
24 delay(300);
25 onLevel = readAvg();
26 if (abs(onLevel - offLevel) >= 100) return;
27 Serial.println(F("Isin LDR'ye ulasmiyor / Beam misses the LDR"));
28 delay(2000);
29 }
30}
31
32int beamPercent() {
33 return (long)(readAvg() - offLevel) * 100 / (onLevel - offLevel);
34}
35
36void setup() {
37 Serial.begin(9600);
38 pinMode(LASER_PIN, OUTPUT);
39 calibrate();
40 Serial.println(F("Sayac hazir / Counter ready"));
41}
42
43void loop() {
44 unsigned long now = millis();
45 int pct = beamPercent();
46
47 if (beamOk && pct < 40) {
48 beamOk = false;
49 brokenSince = now; // kesilme anı / moment of the break
50 } else if (!beamOk && pct > 60) {
51 beamOk = true;
52 unsigned long blocked = now - brokenSince; // ışın ne kadar kesik kaldı / how long the beam was blocked
53 if (blocked >= MIN_BREAK_MS) {
54 passCount++;
55 Serial.print(F("Gecis / Pass #"));
56 Serial.print(passCount);
57 Serial.print(F(" Sure / Time: "));
58 Serial.print(blocked);
59 Serial.print(F(" ms"));
60 if (OBJECT_LENGTH_M > 0) {
61 float speed = OBJECT_LENGTH_M / (blocked / 1000.0); // v = L / t
62 Serial.print(F(" Hiz / Speed: "));
63 Serial.print(speed, 2);
64 Serial.print(F(" m/s"));
65 }
66 Serial.println();
67 }
68 }
69}人がどちら向きに通ったかを知りたいなら、10〜15 cm離した平行な2本のビームを使います。外側のビームAが先に遮られ、次に内側のビームBが遮られれば入ってきたことになり、Bが先でAが後なら出ていったことになります。Aだけが遮られ、Bが一度も遮られないままビームが戻った場合は、ドアに近づいたものの気が変わったということなので、数えません。入った人数から出た人数を引くと中にいる人数がわかり、7セグメントディスプレイやI2C LCDディスプレイに表示できます。2本のビームの間隔は、人の体が同時に両方を遮れるくらい狭くしてください。
レースタイマーと速度の測定
2つのレーザーゲートのあいだの時間を測れば、ランニング、自転車、おもちゃの車、物理の実験に使える正確なストップウォッチになります。スタートとゴールのゲートを既知の距離(d)だけ離して設置すれば、速度はv = d / tです。10 mのコースを2.5秒で走る人は秒速4 m、つまり時速14.4 kmで走っています。2つのレーザーを一緒にオン・オフするには、それぞれの−ピンを1つの2N2222またはS8050トランジスタのコレクタにつなぎます。Sピンと中央のピンは5Vに、トランジスタのエミッタはGNDに、ベースは1 kΩを介してD7につなぎます。スタートゲートのLDRはA0に、ゴールゲートのLDRはA1につなぎます。
1// İki lazer kapılı yarış zamanlayıcı ve hız ölçer / Two-gate laser race timer and speed meter
2const int LASER_PIN = 7; // iki lazer tek transistörle / both lasers through one transistor
3const int START_LDR = A0; // başlangıç kapısı / start gate
4const int FINISH_LDR = A1; // bitiş kapısı / finish gate
5const int BUZZER_PIN = 8; // aktif buzzer / active buzzer
6const float DISTANCE_M = 10.0; // kapılar arası mesafe (metre) / distance between the gates (m)
7const unsigned long TIMEOUT_MS = 60000; // 60 sn'de bitmezse iptal / cancel after 60 s
8
9int onA, offA, onB, offB;
10enum State { READY, RUNNING, DONE };
11State state = READY;
12unsigned long startUs = 0;
13unsigned long startMs = 0;
14unsigned long doneMs = 0;
15
16int readAvg(int pin) {
17 long sum = 0;
18 for (int i = 0; i < 4; i++) sum += analogRead(pin);
19 return sum / 4;
20}
21
22bool isBroken(int pin, int on, int off) {
23 long pct = (long)(readAvg(pin) - off) * 100 / (on - off);
24 return pct < 40; // %40'ın altı = kesik / below 40% = broken
25}
26
27void calibrate() {
28 while (true) {
29 digitalWrite(LASER_PIN, LOW); // iki lazer de kapalı / both lasers off
30 delay(300);
31 offA = readAvg(START_LDR);
32 offB = readAvg(FINISH_LDR);
33 digitalWrite(LASER_PIN, HIGH); // iki lazer de açık / both lasers on
34 delay(300);
35 onA = readAvg(START_LDR);
36 onB = readAvg(FINISH_LDR);
37 if (abs(onA - offA) >= 100 && abs(onB - offB) >= 100) return;
38 Serial.println(F("Kapilardan biri hizali degil / One gate is misaligned"));
39 delay(2000);
40 }
41}
42
43void beep(int ms) {
44 digitalWrite(BUZZER_PIN, HIGH);
45 delay(ms);
46 digitalWrite(BUZZER_PIN, LOW);
47}
48
49void setup() {
50 Serial.begin(9600);
51 pinMode(LASER_PIN, OUTPUT);
52 pinMode(BUZZER_PIN, OUTPUT);
53 calibrate();
54 Serial.println(F("Hazir: baslangic kapisini gec / Ready: cross the start gate"));
55}
56
57void loop() {
58 if (state == READY && isBroken(START_LDR, onA, offA)) {
59 startUs = micros(); // zamanı hemen al / grab the time immediately
60 startMs = millis();
61 state = RUNNING;
62 beep(30);
63 } else if (state == RUNNING) {
64 if (isBroken(FINISH_LDR, onB, offB)) {
65 unsigned long elapsedUs = micros() - startUs;
66 float seconds = elapsedUs / 1000000.0;
67 float speed = DISTANCE_M / seconds; // v = d / t
68 Serial.print(F("Sure / Time: "));
69 Serial.print(seconds, 3);
70 Serial.print(F(" s Hiz / Speed: "));
71 Serial.print(speed, 2);
72 Serial.print(F(" m/s = "));
73 Serial.print(speed * 3.6, 1);
74 Serial.println(F(" km/h"));
75 beep(300);
76 state = DONE;
77 doneMs = millis();
78 } else if (millis() - startMs > TIMEOUT_MS) {
79 Serial.println(F("Zaman asimi / Timeout"));
80 state = READY;
81 }
82 } else if (state == DONE && millis() - doneMs > 3000 &&
83 !isBroken(START_LDR, onA, offA) && !isBroken(FINISH_LDR, onB, offB)) {
84 state = READY; // 3 sn sonra yeni tur / new run after 3 s
85 Serial.println(F("Hazir / Ready"));
86 }
87}このスケッチはステートマシンとして動きます。READY状態でスタートのビームが遮られるとmicros()で時刻を記録してRUNNINGに移り、ゴールのビームが遮られると経過時間と速度を表示し、3秒後に次の走行の準備をします。60秒以内にゴールのゲートを通過しなければ、その走行は取り消されます。精度について正直に言うと、コード自体は1 msより細かい分解能で測りますが、LDRが暗さに反応するには10〜30 msかかります。幸い、両方のゲートで同じ種類のLDRを使うので、この遅れは両端で起こってほぼ打ち消し合います。ランニングや自転車なら、結果は十分すぎるほどです。
ESP32でスマホに通知(Telegram)
家にいないときにも警報を役立てたいなら、ESP32を使えます。ビームが遮られた瞬間に、Telegram経由でスマホにメッセージを送ります。配線は上の表のとおりです。レーザーは5Vから給電し、GPIO 26につないだNPNトランジスタでスイッチングします。LDRの分圧回路は3V3から給電し、GPIO 34につなぎます。このピンの選択には理由があります。ESP32のADC2のピンはWi-Fiがオンのあいだ使えないので、アナログセンサーは必ずADC1のピン(GPIO 32〜39)につなぎます。ESP32のADCは12ビットで0〜4095の値を返すため、コード内のキャリブレーションの最小差は400になっています。ボットの作成とトークン・チャットIDの取得は「ESP32のTelegramボット」のガイドで詳しく解説しています。スケッチのために、ライブラリマネージャからUniversalTelegramBotとArduinoJsonライブラリをインストールしてください。
1// ESP32 lazer bariyeri + Telegram bildirimi / ESP32 laser tripwire + Telegram notification
2#include <WiFi.h>
3#include <WiFiClientSecure.h>
4#include <UniversalTelegramBot.h> // ArduinoJson kütüphanesi de gerekir / also needs the ArduinoJson library
5
6const char* WIFI_SSID = "WIFI_ADI"; // Wi-Fi ağının adı / your Wi-Fi name
7const char* WIFI_PASS = "WIFI_SIFRESI"; // Wi-Fi şifren / your Wi-Fi password
8#define BOT_TOKEN "1234567890:ABCdefGhIJKlmNoPQRstuVWXyz" // BotFather'dan / from BotFather
9#define CHAT_ID "123456789" // senin chat ID'n / your chat ID
10
11const int LASER_PIN = 26; // NPN transistör bazına 1 kΩ ile / to the NPN base via 1 kΩ
12const int LDR_PIN = 34; // ADC1 pini, Wi-Fi ile çalışır / ADC1 pin, works with Wi-Fi
13const int BUZZER_PIN = 25; // aktif buzzer / active buzzer
14const unsigned long CONFIRM_MS = 30; // en az bu kadar kesik kalmalı / must stay broken this long
15const unsigned long SIREN_MS = 5000; // siren süresi / siren duration
16const unsigned long COOLDOWN_MS = 30000; // iki deneme arası en az 30 sn / at least 30 s between attempts
17
18WiFiClientSecure client;
19UniversalTelegramBot bot(BOT_TOKEN, client);
20
21int onLevel = 0, offLevel = 0;
22bool beamOk = true;
23bool counted = false; // bu kesinti sayıldı mı / has this break been counted
24unsigned long brokenSince = 0;
25bool sirenOn = false;
26unsigned long sirenStart = 0;
27unsigned long breakCount = 0;
28bool pending = false; // gönderilecek bildirim var / a notification is waiting
29bool triedOnce = false;
30unsigned long lastTry = 0;
31
32int readAvg() {
33 long sum = 0;
34 for (int i = 0; i < 16; i++) sum += analogRead(LDR_PIN); // 16 ölçüm ortalaması / average of 16 readings
35 return sum / 16;
36}
37
38void calibrate() {
39 while (true) {
40 digitalWrite(LASER_PIN, LOW); // lazer kapalı = kesik ışın / laser off = broken beam
41 delay(300);
42 offLevel = readAvg();
43 digitalWrite(LASER_PIN, HIGH); // lazer açık = sağlam ışın / laser on = intact beam
44 delay(300);
45 onLevel = readAvg();
46 Serial.printf("Isinli / beam: %d Isinsiz / no beam: %d\n", onLevel, offLevel);
47 if (abs(onLevel - offLevel) >= 400) return; // 12 bit ölçekte yeterli fark / enough gap on the 12-bit scale
48 Serial.println("Isin LDR'ye ulasmiyor / Beam misses the LDR");
49 delay(2000);
50 }
51}
52
53int beamPercent() {
54 return (long)(readAvg() - offLevel) * 100 / (onLevel - offLevel);
55}
56
57void setup() {
58 Serial.begin(115200);
59 pinMode(LASER_PIN, OUTPUT);
60 pinMode(BUZZER_PIN, OUTPUT);
61 calibrate();
62
63 WiFi.mode(WIFI_STA);
64 WiFi.begin(WIFI_SSID, WIFI_PASS);
65 client.setCACert(TELEGRAM_CERTIFICATE_ROOT); // api.telegram.org kök sertifikası / root certificate
66 unsigned long start = millis();
67 while (WiFi.status() != WL_CONNECTED && millis() - start < 15000) { // en fazla 15 sn bekle / wait up to 15 s
68 delay(500);
69 Serial.print(".");
70 }
71 if (WiFi.status() == WL_CONNECTED) {
72 Serial.println(WiFi.localIP());
73 bot.sendMessage(CHAT_ID, "Lazer alarmi devrede / Laser alarm armed", "");
74 }
75}
76
77void loop() {
78 unsigned long now = millis();
79 int pct = beamPercent();
80
81 if (beamOk && pct < 40) { // ışın yeni kesildi / beam just broke
82 beamOk = false;
83 counted = false;
84 brokenSince = now;
85 } else if (!beamOk && pct > 60) {
86 beamOk = true; // ışın geri geldi / beam restored
87 }
88
89 if (!beamOk && !counted && now - brokenSince >= CONFIRM_MS) {
90 counted = true; // her kesinti bir kez sayılır / each break counts once
91 breakCount++;
92 pending = true;
93 sirenOn = true;
94 sirenStart = now;
95 Serial.printf("Kesinti / Break #%lu\n", breakCount);
96 }
97
98 if (sirenOn && now - sirenStart >= SIREN_MS) sirenOn = false;
99 digitalWrite(BUZZER_PIN, (sirenOn && (now / 200) % 2) ? HIGH : LOW); // kesik siren / beeping siren
100
101 if (pending && (!triedOnce || now - lastTry >= COOLDOWN_MS)) {
102 triedOnce = true;
103 lastTry = now;
104 if (WiFi.status() == WL_CONNECTED) {
105 String text = "Lazer bariyeri kesildi! / Laser beam broken! Toplam / Total: " + String(breakCount);
106 if (bot.sendMessage(CHAT_ID, text, "")) pending = false; // gitmezse 30 sn sonra yeniden / retry in 30 s if it fails
107 } else {
108 WiFi.reconnect(); // bağlantı koptuysa yeniden dene / reconnect if the link dropped
109 }
110 }
111}遮断は1回ごとに1回だけ数えられ、サイレンが5秒間鳴り、通知が送信待ちに入ります。通知を試みるのは最大でも30秒に1回で、そのあいだに起きた新しい遮断は合計に加えられ、次のメッセージに表示されます。こうすれば、誰かがビームの中を行ったり来たりしても、スマホがメッセージであふれることはありません。メッセージを送れなかった場合、コードは30秒後に再試行し、Wi-Fiが切れていれば再接続を始めます。sendMessage()は安全なHTTPS接続を開くので1〜2秒かかり、そのあいだサイレンのパターンは止まり、ビームも監視されませんが、これは正常な動作です。
Raspberry Pi PicoとMicroPythonでレーザーバリア
Raspberry Pi Picoでは、同じロジックを数行のMicroPythonで作れます。レーザーはVBUS(USBの5 V、40番ピン)から給電し、GP15につないだNPNトランジスタでスイッチングします。LDRの分圧回路は3V3(OUT)ピンから給電してGP26(ADC0)につなぎ、アクティブブザーはGP14につなぎます。PicoのADCはread_u16()で0〜65535の値を返すので、キャリブレーションの最小差は6000です。コードは同じパーセントの計算、ヒステリシス、確認時間を使い、遮断をすべて数え、サイレンを5秒間鳴らし、ビームが遮られていた時間を表示します。パソコンなしでPicoを動かすには、Thonnyでファイルをmain.pyとしてボードに保存してください。
1# Raspberry Pi Pico lazer bariyeri: alarm + geçiş sayacı / Laser barrier: alarm + pass counter
2from machine import Pin, ADC
3import time
4
5laser = Pin(15, Pin.OUT) # NPN transistör bazına 1 kΩ ile / to the NPN base via 1 kΩ
6ldr = ADC(26) # GP26 = ADC0, bölücü 3V3'ten / divider fed from 3V3
7buzzer = Pin(14, Pin.OUT) # aktif buzzer / active buzzer
8
9CONFIRM_MS = 30 # en az bu kadar kesik kalmalı / must stay broken this long
10SIREN_MS = 5000 # siren süresi / siren duration
11
12
13def read_avg(n=8):
14 total = 0
15 for _ in range(n):
16 total += ldr.read_u16()
17 return total // n
18
19
20def calibrate():
21 while True:
22 laser.value(0) # lazer kapalı = kesik ışın / laser off = broken beam
23 time.sleep_ms(300)
24 off = read_avg(32)
25 laser.value(1) # lazer açık = sağlam ışın / laser on = intact beam
26 time.sleep_ms(300)
27 on = read_avg(32)
28 print("Isinli / beam:", on, " Isinsiz / no beam:", off)
29 if abs(on - off) > 6000: # 16 bit ölçekte yeterli fark / enough gap on the 16-bit scale
30 return on, off
31 print("Isin LDR'ye ulasmiyor / Beam misses the LDR")
32 time.sleep(2)
33
34
35on_level, off_level = calibrate()
36beam_ok = True
37counted = False
38broken_at = time.ticks_ms()
39siren_until = time.ticks_ms()
40count = 0
41
42while True:
43 now = time.ticks_ms()
44 pct = (read_avg() - off_level) * 100 // (on_level - off_level) # 100 = ışın tam / full beam
45
46 if beam_ok and pct < 40: # ışın yeni kesildi / beam just broke
47 beam_ok = False
48 counted = False
49 broken_at = now
50 elif not beam_ok and pct > 60: # ışın geri geldi / beam restored
51 beam_ok = True
52 print("Kesik kaldigi sure / blocked for", time.ticks_diff(now, broken_at), "ms")
53
54 if not beam_ok and not counted and time.ticks_diff(now, broken_at) >= CONFIRM_MS:
55 counted = True # her kesinti bir kez sayılır / each break counts once
56 count += 1
57 siren_until = time.ticks_add(now, SIREN_MS)
58 print("Gecis / Pass:", count)
59
60 siren = time.ticks_diff(siren_until, now) > 0
61 buzzer.value(1 if siren and (now // 200) % 2 else 0) # kesik siren / beeping sirenレーザーモジュールを使ったプロジェクトのアイデア
- レーザーのホームアラーム:ドアと廊下のビームをPIR人感センサーと組み合わせ、すべての入力を1つの制御ユニットにまとめます。詳しくは「Arduinoのアラームシステム」のガイドで解説しています。
- レーザーハープ:5〜7本のビームを並べ、ビームが遮られるたびにパッシブブザーで違う音を鳴らします。
- 来場者カウンター:2本のビームで入場と退場を区別し、中にいる人数をディスプレイに表示します。
- 科学コンテスト向けの速度計:2つのゲートで斜面を滑り落ちる物体の速度を測り、摩擦を比べます。ほかのアイデアは「Arduinoの科学コンテスト向けプロジェクトアイデア」のガイドをご覧ください。
- レーザー迷路ゲーム:鏡を使ってひざより低い位置にビームの網を張り、触れずに通り抜けようとするプレイヤーのタイムを測り、触れるたびにペナルティを加えます。
- 引き出しや金庫の見張り:ふたが開いてビームが遮られたことを検知し、ESP32でスマホに通知を送ります。
よくある間違いと対処法
| 症状 | 考えられる原因 | 対処法 |
|---|---|---|
| レーザーがまったく点かない | Sと−が逆、または中央のピンに電源が必要な基板 | ピンの表示を確認し、中央のピンを5Vにつないでもう一度試す |
| レーザーがとても暗い | モジュールを3.3 Vで給電している、またはピンの電流が足りない | レーザーを5Vから給電し、トランジスタでスイッチングする |
| 「Isin LDR'ye ulasmiyor」(トルコ語で「ビームがLDRに届かない」)というメッセージ | 点がLDRに当たっていない、または周囲の光が強すぎる | 紙で点を見つけ、LDRを黒い筒に入れる |
| 警報が鳴りっぱなしになる | 光軸がずれた、レーザーや鏡がぐらついている | ホルダーをしっかり固定し、シリアルプロッタで読み取り値を確認する |
| 警報がまったく鳴らない | ビームが遮られても、日光や照明でLDRが明るいまま | 筒を付け、センサーを窓と反対に向ける |
| 速い通過が数えられない | LDRの応答時間(10〜30 ms)が遅すぎる | CONFIRM_MSとMIN_BREAK_MSを下げるか、フォトトランジスタを使う |
| Arduinoがリセットする、またはピンが熱くなる | 1本のピンで複数のレーザーを駆動している | レーザーを5Vから給電し、トランジスタでスイッチングする |
| ESP32でWi-Fiが始まると読み取り値がおかしくなる | LDRがADC2のピンにつながっている | LDRをGPIO 32〜39のADC1のピンに移す |
| Telegramのメッセージが届かない | トークンかチャットIDの間違い、またはボットに一度もメッセージを送っていない | ボットに/startを送り、トークンとチャットIDをもう一度確認する |
KY-008レーザーモジュールは、安い部品1つで光学、分圧回路、キャリブレーション、時間の測定をまとめて学べる、数少ないプロジェクトを可能にしてくれます。まずテスト用のスケッチで差を確かめ、次に警報を作り、鏡、カウンター、タイマーでプロジェクトを広げていきましょう。BlueGraysアプリでは、レーザー警報システム、LDRで光の強さを測定、LDRで作る常夜灯、ESP32の動体検知アラーム、Telegramで通知するプロジェクトを、回路図とコードつきでステップごとに見つけられます。
よくある質問
KY-008レーザーモジュールには何Vの電圧が必要ですか?
KY-008は5 V用に設計されていて、モジュールの電流制限抵抗のおかげでArduino Unoのデジタルピンに直接つなげます。3.3 Vでも点灯しますが、暗くなります。モジュールに5 Vを超える電圧をかけたり、抵抗なしでレーザーダイオードに給電したりすることは絶対にしないでください。
5 mWのレーザーで目を傷めることはありますか?
5 mWの赤色レーザーはクラス3Rです。短時間うっかり当たる程度なら、たいてい後遺症は残りませんが、ビームを見つめたり鏡の反射光が目に入ったりすると、網膜を傷めることがあります。ビームは目の高さより下に保ち、人や動物には決して向けず、鏡を使う構成ではビームの経路全体を確認してください。
レーザー侵入警報はどれくらいの距離で使えますか?
距離を制限するのは、レーザーの出力よりも光軸合わせと周囲の光です。しっかりしたホルダーと黒い筒に入れたLDRがあれば、屋内では10〜20 mでも問題なく動きます。それより長い距離では点が大きくなり、LDRに当たる光が減ります。屋外では日光、霧、風による揺れで、使える距離がはっきり短くなります。
レーザーバリアでLDRの代わりに使えるものは?
LDRは安くて手軽ですが、応答時間は10〜30 msです。より速い測定が必要なタイマーやカウンターには、マイクロ秒単位で反応するフォトトランジスタやフォトダイオードが使われます。市販のレーザー受光モジュールもあり、光を受けるとデジタル出力を出すので、デジタルピンに直接つなげます。
レーザーモジュールはESP32やRaspberry Pi Picoでも使えますか?
はい。ただし、ピンから直接レーザーに給電するより、5Vから給電してNPNトランジスタでスイッチングするほうがよいです。Picoのピンが出せる電流は初期設定で4 mAしかありません。これらのボードではLDRの分圧回路を3V3から給電し、ESP32ではWi-Fiと同時に使えるADC1のピン(GPIO 32〜39)を使ってください。
赤外線のビームバリアはレーザー警報より優れていますか?
プロの防犯システムでは、目に見えない赤外線のビームと変調方式の受光器が使われています。ビームを見つけられず、変調のおかげで日光の影響もほとんど受けません。一方、赤色レーザーは目に見えるので光軸合わせがとても簡単で、学習用のプロジェクトやカウンター、タイマーに最適です。
この記事を読んだ後に作れるプロジェクト
全プロジェクトを見る
LDRで光の強さを測定
部屋の明るさを数値で見てみませんか?光に反応する小さな部品、LDRで周囲の光を測定し、Arduinoが0.5秒ごとにその値をコンピューターの画面に表示します。手で覆うと数値が変わる様子を確認できます。自動で点灯する常夜灯などのプロジェクトへの第一歩です。

LDRで作る常夜灯
暗くなると自動で点灯し、朝になると消灯する小さな常夜灯です。光センサーが部屋の明るさを測定し、Arduinoが十分に暗くなったらLEDを点灯させ、明るくなったら消灯させます。街灯が夕方になると自動で点灯する仕組みも理解できます。

ESP32とLDRモジュールで光の強さを読み取る
LDRモジュールの生のADC値と、暗い状態・明るい状態の基準値から計算した相対的な明るさの割合を、シリアルモニタでリアルタイムに確認します。この割合はルクスの測定値ではありません。モジュールの校正の仕組みを学べます。しきい値の決定に使うため、プロジェクト10の前に取り組むのが適しています。

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

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

ESP32とLDRで作るナイトライト
暗くなるとLEDが自動で点灯し、明るくなると消灯するナイトライトです。LDRセンサーモジュールのD0(デジタル出力)ピンを使います。モジュール上の小さな可変抵抗で暗さのしきい値を設定すると、モジュール自体がHIGH/LOWを出力します。ESP32はdigitalRead()でこの信号を読み取るだけなので、コードはとてもシンプルです。

ESP32動体検知アラーム(PIR + ブザー + LED + Telegram)
PIRセンサーが動きを検知するとブザーとLEDを作動させ、同時にTelegramで持ち主に即座に通知するスマート防犯アラームです。アラームが繰り返される頻度を抑えるため、30秒のクールダウン(待ち時間)を設けています。この仕組みはPIRの検知が正しいかを確認したり、誤検知をなくしたりするものではありません。家の防犯、ガレージの監視、倉庫や作業部屋などに使えます。ESP32-CAMなら写真付きの通知を追加でき、通常のESP32 DevKit V1ではテキストで通知します。
ほかの記事
BlueGraysアプリ
この回路をスマホでステップごとに作る
回路図、コード、部品リストを1つの画面で確認できます。Arduino、ESP32、Picoのプロジェクトを200件以上用意しています。

