Arduinoで作るスマートゴミ箱:HC-SR04、サーボのふた、満杯アラーム
Arduinoでスマートゴミ箱を作ります。HC-SR04での手の検知、ゆっくり閉まるサーボのふた、ゴミの量を示すLEDとブザー、電池の計画、ESP32からのTelegram通知まで解説します。
Arduinoで作るスマートゴミ箱は、ふたに触れずに開けられるシンプルながら学びの多い自動化プロジェクトで、学校の課題としても特に人気があります。ゴミ箱の前面に付けたHC-SR04超音波センサーが近づいてくる手を検知し、Arduinoがサーボモーターでふたを開けます。手を離して数秒たつと、ふたをバタンと落とさずにゆっくり閉じます。ふたの裏に付けた2つ目のHC-SR04はゴミの高さを測ります。ゴミがたまるにつれて緑、黄、赤のLEDが順に点灯し、完全にいっぱいになるとブザーが鳴ります。
このガイドでは部品リストから始めて、センサーを取り付ける位置、サーボのトルクの計算方法、ふたをヒンジ(蝶番)に取り付ける方法、キャリブレーション、そして待ち時間とゆっくり閉じる動作を同時に扱う完全なコードを順を追って説明します。続いて、電池でプロジェクトが実際にどれくらい動くかを計算し、ESP32でゴミの量の通知をTelegramのメッセージとしてスマートフォンに送り、同じプロジェクトをRaspberry Pi PicoとMicroPythonで書きます。最後には、学校の課題の発表で使える構成案と、自分の測定値で埋められるテスト表もあります。

スマートゴミ箱のしくみ
このプロジェクトは1秒間に何十回も繰り返されるループです。Arduinoは手検知センサーで60ミリ秒ごとに測定します。20 cmより近い物体が2回続けて検知されると、それを手と判断してサーボを開いた位置へ動かします。手がそこにある間は、測定のたびに待ち時間がリセットされます。手が離れてから4秒後、ふたは毎秒約50度の速さでゆっくり閉まります。閉まっている途中で再び手が近づくと、ふたはすぐに開き直します。ふたが閉まって1秒たつと、ゴミ量センサーが5回測定し、その中央値をゴミの表面までの距離とみなしてパーセントに換算します。コードでは小さなステートマシンを使い、ふたは常にCLOSED(閉)、OPEN(開)、CLOSING(閉じている途中)の3つの状態のどれかにあります。
| 部品 | 役割 | メモ |
|---|---|---|
| Arduino Uno(またはNano) | 頭脳:測定して判断し、サーボとLEDを動かす | ESP32やPicoでも可 |
| HC-SR04 × 2個 | 1つは手を検知し、もう1つはゴミの量を測る | 5 V、測定中は約15 mA |
| SG90、MG90SまたはMG996Rのサーボ | ふたを開け閉めする | ふたの重さで選ぶ(計算は後述) |
| LED 3個(緑、黄、赤)と220 Ω × 3個 | ゴミの量の表示 | LED 1個につき抵抗1本 |
| アクティブブザー | 満杯の警告 | パッシブブザーならtone()が必要 |
| ヒンジ、ねじ、ホットボンド | ふたを支え、サーボホーンを固定する | ふたの重さはサーボではなくヒンジで支える |
| 470 µFのコンデンサ | サーボの起動電流をなだらかにする | サーボの+と−の間に、極性に注意 |
| 電源 | 5 VのACアダプター、モバイルバッテリー、電池 | 動作時間の計算は後述 |
| ESP32(任意) | ゴミの量をスマートフォンに通知 | Wi-Fiが必要 |




手の検知にはどのセンサーを使う?
ゴミ箱を開けるセンサーはいくつかありますが、どれもこの用途に同じように向いているわけではありません。本当に必要なのは、ゴミ箱から15〜25 cm以内に伸びてきた手には反応し、前を歩いて通り過ぎるだけの人は無視することです。HC-SR04は距離をセンチメートル単位で返すので、しきい値をコードで自由に設定できます。スマートゴミ箱のプロジェクトの多くでHC-SR04が使われているのはそのためです。ほかの選択肢の長所と短所は次のとおりです。
| センサー | 検知のしかた | ゴミ箱での評価 |
|---|---|---|
| HC-SR04(超音波) | 音の反響、2〜400 cm、狭い円錐状 | しきい値をコードで設定でき、最もバランスがよい |
| 赤外線障害物センサー(FC-51) | 反射した赤外線、約2〜30 cm | 安くて速いが、日光や黒い手袋でだまされることがある |
| PIR(HC-SR501) | 体温の動き、最大7 m、広角 | 前を通る人すべてに反応し、数秒間出力が続くため不向き |
| APDS-9960 | 近距離の近接検知と手のジェスチャー | 手を振って開ける楽しい版。I2Cと3.3 Vが必要 |
PIRセンサーのしくみはPIR人感センサーの記事で、ジェスチャーで操作する版はAPDS-9960ジェスチャーセンサーのガイドで紹介しています。HC-SR04のしくみを簡単に説明すると、40 kHzの短い音を発し、反響が戻るまでの時間をECHOピンのパルスとして出力します。20 °Cの空気中で音は約343 m/sで進み、行きと帰りの往復になるので、マイクロ秒単位の時間を58で割るとセンチメートル単位の距離になります。詳しくはHC-SR04距離センサーの記事をご覧ください。
センサーの配置
手検知センサーは、ふたではなくゴミ箱の本体の前面上部に付けます。ふたに付けると、ふたが開いたときにセンサーも一緒に回って天井を向いてしまうからです。センサーを10〜20度上向きに傾けると、ゴミ箱の上に伸ばした手はとらえつつ、前を通る人の脚は拾わなくなります。ゴミ箱を壁や棚の横に置く場合は、センサーが向いている方向の30 cm以内に固定された面があってはいけません。あると、その面を手と勘違いして、ふたがいつまでも閉まりません。
ゴミ量センサーは、ふたの裏の中央付近に真下を向けて貼り付けます。この位置では、ふたが閉まっているときにしか意味のある測定ができません。ふたが開いていると、センサーは壁や天井を向いてしまいます。そのため、コードではふたが閉まってから1秒後にだけゴミの量を測ります。センサーの音の広がりは狭いものの(データシートでは15度、実際にはもう少し広い)、深さ35 cmでは直径10〜20 cmの範囲をカバーします。幅が20 cmより狭いゴミ箱では、音が側面で反射して誤った値になることがあります。ゴミ袋の口はゴミ箱の外側に折り返してください。内側に垂れた袋のふちがセンサーの前に入ると、空でもいっぱいに見えてしまいます。
配線図:Arduino Uno、ESP32、Pico
下の表は、この記事のすべてのコードと完全に一致しています。Unoでは手検知センサーをD2〜D3、ゴミ量センサーをD4〜D5、サーボをD9、LEDをD10〜D12、ブザーをD8につなぎます。UnoではServoライブラリがTimer1を使うため、D9とD10のPWM出力が使えなくなりますが、D10のLEDはオンとオフを切り替えるだけなので影響はありません。各LEDの前には220 Ωの抵抗を入れます。自分のLEDに合った値はLED抵抗ツールで計算できます。ボードのピン配置はピン配置ツールで確認できます。
| 接続 | Arduino Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|
| 手検知センサーのTRIG | D2 | GPIO 18 | GP2 |
| 手検知センサーのECHO | D3(直結) | GPIO 19、1 kΩ / 2 kΩの分圧回路経由 | GP3、1 kΩ / 2 kΩの分圧回路経由 |
| ゴミ量センサーのTRIG | D4 | GPIO 25 | GP4 |
| ゴミ量センサーのECHO | D5(直結) | GPIO 26、分圧回路経由 | GP5、分圧回路経由 |
| HC-SR04のVCC(両方) | 5V | 5V(VIN) | VBUS |
| サーボの信号(オレンジ) | D9 | GPIO 13 | GP15 |
| サーボの+(赤) | 5V(SG90)または外部5〜6 V | 5V(VIN)または外部5〜6 V | VBUSまたは外部5〜6 V |
| 緑、黄、赤のLED(220 Ω付き) | D10、D11、D12 | GPIO 27、32、33 | GP10、GP11、GP12 |
| アクティブブザー(+) | D8 | GPIO 23 | GP13 |
| すべてのGND | GND | GND | GND |
サーボの選び方とトルクの計算
スマートゴミ箱のプロジェクトで最も多いトラブルは、ふたが途中まで開いて震える、あるいはまったく持ち上がらないというものです。原因はほぼ必ず、ふたに対してサーボが非力すぎることです。必要なトルクの見積もりは簡単です。トルク(kg·cm)=ふたの重さ(kg)× ヒンジから重心までの距離(cm)。厚さが均一な平らなふたなら、重心はちょうど中央にあります。長さ24 cmのふたなら、その距離は12 cmです。いちばん負荷が大きいのは、ふたが水平な開き始めの瞬間です。ふたが持ち上がるにつれて必要なトルクは減り、垂直になるとほぼゼロになります。サーボのカタログに載っているトルクは停動トルク(ストールトルク)、つまりサーボが力んで止まってしまう点の値です。この限界近くで動かし続けるとサーボは発熱し、ギアがすり減ります。そのため、計算結果を少なくとも2倍にしてください。
| ふた | 重さ | 重心 | 必要なトルク | 2倍の余裕込み | おすすめ |
|---|---|---|---|---|---|
| 20 × 20 cmの段ボールまたはスチレンボード | 50 g | 10 cm | 0.5 kg·cm | 1.0 kg·cm | SG90 |
| 25 cmのプラスチック製ゴミ箱のふた | 120 g | 12.5 cm | 1.5 kg·cm | 3.0 kg·cm | MG996R(MG90Sはぎりぎり) |
| 30 cmのMDFまたは合板のふた | 300 g | 15 cm | 4.5 kg·cm | 9.0 kg·cm | MG996R+カウンターウェイト |
| サーボ | トルク(4.8 V / 6 V) | ギア | 電流 | メモ |
|---|---|---|---|---|
| SG90 | 1.8 kg·cm(4.8 Vの値のみ公表) | プラスチック | 動作中100〜250 mA | Arduinoの5Vピンから給電できる |
| MG90S | 1.8 / 2.2 kg·cm | 金属 | 動作中150〜300 mA | SG90と同じサイズで、より丈夫 |
| MG996R | 9.4 / 11 kg·cm | 金属 | 動作中0.5〜0.9 A、停動時は最大2.5 A | 別の5〜6 V電源から給電する必要がある |

より強いサーボを買う代わりに、ふたのバランスをとるだけで解決することもよくあります。ふたにヒンジの後ろへ伸びる短い腕があれば、そこにボルトや硬貨をいくつか貼り付けると重さの一部がつり合います。このカウンターウェイトで、必要なトルクを最大で半分まで減らせます。サーボホーンでヒンジから離れた位置を押す方法もありますが、てこでトルクがただで得られるわけではありません。ヒンジから離れるほど必要な力は小さくなりますが、押す点の移動距離が長くなるので長いサーボホーンが必要になり、長いホーンはサーボに求めるトルクを再び大きくします。ふたを90度近くまで開けたいなら、必要なサーボのトルクはヒンジに直接取り付けた場合とほとんど変わりません。サーボの種類の詳しい比較はサーボモーターとは?とMG996Rサーボの記事にあります。
取り付けのコツ:ヒンジ、サーボホーン、ケーブル
- 重さはヒンジで支える:ふたは本物のヒンジ(ゴミ箱に付いているヒンジか、家具用の小さな蝶番)に取り付けます。ふたをサーボの軸だけで支えると、重さがすべてサーボの軸受けにかかり、SG90のプラスチックギアはすぐに壊れてしまいます。
- サーボの軸をヒンジの軸にそろえる:いちばん簡単なのは、サーボの軸がヒンジの線の延長上に来るように、サーボをゴミ箱の後ろのふちにねじで固定し、サーボホーンをねじかホットボンドでふたに留める方法です。軸がずれていると、開くたびにサーボがふたを横方向に押してしまいます。
- サーボ本体をしっかり固定する:サーボを両面テープだけで留めないでください。ふたを持ち上げるときに本体がねじれてしまいます。サーボの取り付け穴に2本のねじを通すか、小さなL字金具を使います。
- ふたを縁に押し付けない:閉じた角度は、ふたが縁に載っていてもサーボが押していない位置に設定します。閉じたふたをサーボが押し続けると、ジーッと音を立てて発熱し、電流を無駄にします。
- ケーブルに余裕を持たせる:ゴミ量センサーの配線はふたと一緒に動きます。ケーブルはヒンジの近くを通し、ふたを全開にしても引っ張られない長さにして、結束バンドで固定します。
- 電子部品をゴミから離す:Arduino、電池、ブレッドボードは、ゴミ箱の背面か底に貼り付けた小さな箱に入れます。ゴミ袋から漏れた液体や湿気が回路に届かないようにしてください。
- まず段ボールの模型で作る:学校の課題なら、靴箱やスチレンボードで作った軽いふたはSG90でも問題なく動きます。発表でトルクの計算を見せる例としてもぴったりです。
セットアップとキャリブレーションの手順
メインのコードに進む前に、下の小さなキャリブレーション用スケッチを書き込みます。このスケッチは、シリアルモニタに入力した角度へサーボを動かし、2つのセンサーが読んだ距離を0.5秒ごとに表示します。これで、自分のゴミ箱に合わせて、ふたの閉じた角度と開いた角度、空のゴミ箱の深さ、手を検知するしきい値を測れます。
1#include <Servo.h>
2
3const int HAND_TRIG = 2, HAND_ECHO = 3; // el sensörü / hand sensor
4const int FILL_TRIG = 4, FILL_ECHO = 5; // doluluk sensörü / fill sensor
5const int SERVO_PIN = 9;
6
7Servo lid;
8unsigned long lastPrint = 0;
9
10int pingCm(int trig, int echo) {
11 digitalWrite(trig, LOW); delayMicroseconds(2);
12 digitalWrite(trig, HIGH); delayMicroseconds(10); // 10 µs tetik darbesi / 10 µs trigger pulse
13 digitalWrite(trig, LOW);
14 unsigned long t = pulseIn(echo, HIGH, 30000UL); // en fazla 30 ms bekle / wait at most 30 ms
15 return t == 0 ? 999 : (int)(t / 58); // 999 = yankı yok / 999 = no echo
16}
17
18void setup() {
19 Serial.begin(9600);
20 pinMode(HAND_TRIG, OUTPUT); pinMode(HAND_ECHO, INPUT);
21 pinMode(FILL_TRIG, OUTPUT); pinMode(FILL_ECHO, INPUT);
22 lid.write(90); // başlangıçta orta konum / start at the middle position
23 lid.attach(SERVO_PIN);
24 Serial.println(F("Açı yaz (0-180) ve Enter'a bas / Type an angle (0-180) and press Enter"));
25}
26
27void loop() {
28 if (Serial.available()) {
29 String s = Serial.readStringUntil('\n');
30 s.trim();
31 if (s.length() > 0) { // boş satırı yok say / ignore empty lines
32 int a = constrain(s.toInt(), 0, 180);
33 lid.write(a);
34 Serial.print(F("Servo: "));
35 Serial.println(a);
36 }
37 }
38
39 if (millis() - lastPrint >= 500) { // yarım saniyede bir yaz / print every half second
40 lastPrint = millis();
41 int hand = pingCm(HAND_TRIG, HAND_ECHO);
42 delay(60); // iki sensör arasında bekle / wait between the two sensors
43 int fill = pingCm(FILL_TRIG, FILL_ECHO);
44 Serial.print(F("El / Hand: "));
45 Serial.print(hand);
46 Serial.print(F(" cm Doluluk / Fill: "));
47 Serial.print(fill);
48 Serial.println(F(" cm"));
49 }
50}- サーボホーンを付けずにコードを書き込み、シリアルモニタを9600ボーで開きます。改行の設定はLFのみ(Newline)にします。サーボは90度の位置に動きます。
- 10と入力してEnterキーを押します。ふたを閉じた状態で押さえ、その位置でサーボホーンを軸にはめてから、ふたに固定します。
- 角度を20、40、60と増やしていき、ふたが完全に開く値を探します。たいていは90〜110度です。ふたを垂直より先まで開けないでください。その値がLID_OPENになります。サーボが逆方向に回る場合は、閉じた値と開いた値をそれに合わせて選びます。コードはどちらの方向でも動きます。
- 小さな角度に戻ります。ふたが縁にぴったり載る角度から、開く方向へ2〜3度戻した値がLID_CLOSEDになります。閉じているときにサーボがジーッと鳴るなら、ふたを押し付けています。
- ゴミ袋をセットした空の状態で、ゴミ量センサーが読む値を記録します。これがEMPTY_CMです。FULL_CMには6〜10 cmを選びます。ゴミがこの高さに達しても、ふたは楽に閉まり、センサーも確実に測定できなければなりません。HC-SR04は2〜3 cmより近い距離を測れないからです。
- 手検知センサーの前に5、10、20、30 cmの位置で手をかざし、読み取り値を見ます。次にゴミ箱の前を歩いて通り過ぎ、読み取られた最も近い値を記録します。HAND_CMはその値より小さくする必要があります。たいていのゴミ箱では15〜25 cmがうまくいきます。
- 求めた値をメインのコードの先頭にある設定に入力し、書き込みます。
メインのコード:手の検知、待ち時間、ゆっくり閉じる動作
メインのコードには長いdelay()がなく、すべてmillis()で記録した時刻を使って処理します。そのため、ふたが閉じている途中でも手検知センサーは読み取りを続け、手が再び近づけばふたはすぐに開き直します。子どもの手がふたに挟まるのを防ぐ、シンプルですが大切な安全機能です。止めない時間管理の詳細はmillis()とdelay()で、状態で考える方法はArduinoのステートマシンの記事で紹介しています。
1#include <Servo.h>
2
3// Pinler / Pins
4const int HAND_TRIG = 2, HAND_ECHO = 3; // el sensörü, kutunun önünde / hand sensor, front of the bin
5const int FILL_TRIG = 4, FILL_ECHO = 5; // doluluk sensörü, kapağın altında / fill sensor, under the lid
6const int SERVO_PIN = 9;
7const int LED_GREEN = 10, LED_YELLOW = 11, LED_RED = 12;
8const int BUZZER = 8; // aktif buzzer / active buzzer
9
10// Ayarlar: kalibrasyon kodunda bulduğun değerleri yaz / Settings: use the values from the calibration sketch
11const int HAND_CM = 20; // bundan yakın el kapağı açar / a hand closer than this opens the lid
12const int LID_CLOSED = 10; // kapak kenara oturuyor, bastırmıyor / lid rests on the rim, not pressing
13const int LID_OPEN = 110; // kapak tam açık / lid fully open
14const unsigned long OPEN_TIME = 4000; // son el algılamadan sonra açık kalma (ms) / stay open after the last hand (ms)
15const unsigned long CLOSE_STEP_MS = 20; // kapanırken derece başına ms / ms per degree while closing
16const int EMPTY_CM = 35; // boş kutuda sensörden dibe / sensor to bottom when empty
17const int FULL_CM = 8; // bu mesafede kutu %100 dolu / the bin is 100% full at this distance
18
19enum LidState { CLOSED, OPEN, CLOSING };
20LidState state = CLOSED;
21Servo lid;
22
23unsigned long lastHand = 0, lastHandPing = 0, closeStart = 0, closedAt = 0, lastFill = 0;
24int fillPercent = 0;
25byte handHits = 0;
26bool fullWarned = false;
27
28int pingCm(int trig, int echo) {
29 digitalWrite(trig, LOW); delayMicroseconds(2);
30 digitalWrite(trig, HIGH); delayMicroseconds(10); // 10 µs tetik darbesi / 10 µs trigger pulse
31 digitalWrite(trig, LOW);
32 unsigned long t = pulseIn(echo, HIGH, 30000UL); // en fazla 30 ms bekle / wait at most 30 ms
33 return t == 0 ? 999 : (int)(t / 58); // 999 = yankı yok / 999 = no echo
34}
35
36int medianCm(int trig, int echo) { // 5 ölçümün ortancası / median of 5 readings
37 int v[5];
38 delay(60); // önce el sensörünün yankısı sönsün / first let the hand sensor's echo die out
39 for (int i = 0; i < 5; i++) {
40 v[i] = pingCm(trig, echo);
41 delay(60); // yankılar sönsün / let the echoes die out
42 }
43 for (int i = 0; i < 4; i++) // küçükten büyüğe sırala / sort ascending
44 for (int j = i + 1; j < 5; j++)
45 if (v[j] < v[i]) { int tmp = v[i]; v[i] = v[j]; v[j] = tmp; }
46 return v[2];
47}
48
49int toPercent(int cm) { // mesafe → doluluk yüzdesi / distance → fill percent
50 long p = (long)(EMPTY_CM - cm) * 100 / (EMPTY_CM - FULL_CM);
51 if (p < 0) p = 0;
52 if (p > 100) p = 100;
53 return (int)p;
54}
55
56void showLevel() {
57 digitalWrite(LED_GREEN, fillPercent < 50);
58 digitalWrite(LED_YELLOW, fillPercent >= 50 && fillPercent < 80);
59 digitalWrite(LED_RED, fillPercent >= 80);
60}
61
62void beep(int n) {
63 for (int i = 0; i < n; i++) {
64 digitalWrite(BUZZER, HIGH); delay(120);
65 digitalWrite(BUZZER, LOW); delay(120);
66 }
67}
68
69void openLid() {
70 if (!lid.attached()) lid.attach(SERVO_PIN);
71 lid.write(LID_OPEN); // hızlı aç / open quickly
72 state = OPEN;
73 if (fillPercent >= 90) beep(2); // açılırken dolu uyarısı / full warning on opening
74}
75
76void setup() {
77 Serial.begin(9600);
78 pinMode(HAND_TRIG, OUTPUT); pinMode(HAND_ECHO, INPUT);
79 pinMode(FILL_TRIG, OUTPUT); pinMode(FILL_ECHO, INPUT);
80 pinMode(LED_GREEN, OUTPUT); pinMode(LED_YELLOW, OUTPUT); pinMode(LED_RED, OUTPUT);
81 pinMode(BUZZER, OUTPUT);
82 lid.write(LID_CLOSED); // attach'tan önce: açılışta 90°'ye sıçramasın / before attach: no jump to 90° at power-up
83 lid.attach(SERVO_PIN);
84 delay(500);
85 lid.detach(); // kapalıyken servoyu bırak / release the servo while closed
86 closedAt = millis();
87 showLevel();
88}
89
90void loop() {
91 unsigned long now = millis();
92
93 // 1) El algılama, 60 ms'de bir / Hand detection, every 60 ms
94 if (now - lastHandPing >= 60) {
95 lastHandPing = now;
96 int d = pingCm(HAND_TRIG, HAND_ECHO);
97 if (d >= 2 && d <= HAND_CM) {
98 if (handHits < 2) handHits++;
99 } else {
100 handHits = 0;
101 }
102 if (handHits >= 2) { // art arda 2 ölçüm / 2 readings in a row
103 lastHand = now; // bekleme süresi yeniden başlar / the wait time restarts
104 if (state != OPEN) openLid(); // kapanırken el gelirse yeniden aç / reopen if a hand appears while closing
105 }
106 }
107
108 // 2) Bekleme süresi doldu mu? / Has the wait time passed?
109 if (state == OPEN && now - lastHand >= OPEN_TIME) {
110 state = CLOSING;
111 closeStart = now;
112 }
113
114 // 3) Yavaş kapanma: açı geçen süreden hesaplanır / Slow close: angle is computed from elapsed time
115 if (state == CLOSING) {
116 int moved = (now - closeStart) / CLOSE_STEP_MS;
117 int range = abs(LID_OPEN - LID_CLOSED);
118 if (moved >= range) {
119 lid.write(LID_CLOSED);
120 state = CLOSED;
121 closedAt = now;
122 } else {
123 lid.write(LID_OPEN > LID_CLOSED ? LID_OPEN - moved : LID_OPEN + moved);
124 }
125 }
126
127 // 4) Kapandıktan 0,4 s sonra servoyu bırak / Release the servo 0.4 s after closing
128 if (state == CLOSED && lid.attached() && now - closedAt > 400) lid.detach();
129
130 // 5) Doluluk: yalnızca kapak kapalı ve durgunken / Fill level: only while the lid is closed and still
131 if (state == CLOSED && now - closedAt > 1000 && now - lastFill >= 3000) {
132 lastFill = now;
133 int cm = medianCm(FILL_TRIG, FILL_ECHO);
134 if (cm < 999) fillPercent = toPercent(cm); // yankı yoksa eski değeri koru / keep the old value if there is no echo
135 showLevel();
136 Serial.print(F("Doluluk / Fill: "));
137 Serial.print(cm);
138 Serial.print(F(" cm = %"));
139 Serial.println(fillPercent);
140 if (fillPercent >= 90 && !fullWarned) { // doldu: bir kez öt / just filled: beep once
141 fullWarned = true;
142 beep(3);
143 }
144 if (fillPercent < 70) fullWarned = false; // boşaltıldı / emptied
145 }
146}| 関数または設定 | 働き |
|---|---|
| pingCm()(1回測定) | 1回測定し、30 ms以内に反響が来なければ999を返す |
| medianCm()(中央値) | 60 ms間隔で5回測定し、並べ替えて真ん中の値を返す |
| toPercent()(パーセント換算) | EMPTY_CMとFULL_CMをもとに、距離を0〜100のパーセントに換算する |
| handHits | 手を検知した測定が何回続いたかを数える。1回の誤検知ではふたは開かない |
| OPEN_TIME | 最後に手を検知してから閉じ始めるまでの時間(4000 ms) |
| CLOSE_STEP_MS | 閉じるときの1度あたりの時間:100度 × 20 ms=2秒で閉じる |
| lid.detach()(サーボ解放) | ふたが閉じてから0.4秒後にサーボへのパルス送信を止める |
| fullWarned | 満杯の警告音を1回だけ鳴らす。ゴミの量が70%を下回るとリセットされる |
ゆっくり閉じるしくみのポイントは、角度を1度ずつ増やすのではなく経過時間から計算することです。閉じ始めて600 ms後には、ふたは600 / 20 = 30度動いているはずです。ゴミの量の測定やブザーでループが一瞬止まっても、コードはふたを正しい角度に置くので、動きがぎくしゃくしません。もっとゆっくり閉じたい場合はCLOSE_STEP_MSを大きくします。30 msなら100度に3秒かかります。ふたが閉じると、サーボはdetach()で解放されます。ふたはすでに縁に載っているので、サーボが支える必要はなく、ジーッと鳴ることもなく、電池の消費も減ります。コードでattach()より先にwrite()を呼んでいるのも意図的です。こうするとArduinoの電源を入れたとき、サーボがいったん90度に跳ねることなく、最初から閉じた位置で動き始めます。
ゴミの量の測定とLED・ブザーの警告
ゴミ量センサーは、ふたの裏からゴミのいちばん高い点までの距離を測ります。この距離はゴミ箱が空のときに最大(EMPTY_CM)、いっぱいのときに最小(FULL_CM)になります。パーセントは次の式で求めます。ゴミの量(%)=(EMPTY_CM − 測定値)/(EMPTY_CM − FULL_CM)× 100。たとえばEMPTY_CM = 35 cm、FULL_CM = 8 cmで測定距離が20 cmなら、ゴミの量は(35 − 20)/ 27 × 100 ≈ 55%となり、黄色のLEDが点灯します。測定値が35 cmより大きい場合(音が底で斜めに反射した場合)は0%に、8 cmより小さい場合は100%に制限されます。
| ゴミの量 | LED | ブザー | 意味 |
|---|---|---|---|
| 0〜49% | 緑 | 鳴らない | 十分な余裕あり |
| 50〜79% | 黄 | 鳴らない | 半分以上たまった。そろそろ空にする |
| 80〜89% | 赤 | 鳴らない | ほぼいっぱい |
| 90〜100% | 赤 | いっぱいになると短く3回、その後は開くたびに2回 | すぐに空にする |
ゴミの表面は平らではありません。センサーの真下にペットボトルが1本立っていると、測定はその点をとらえ、実際よりいっぱいに見えます。メディアンフィルタは飛び抜けた1回の値を取り除きますが、ゴミの形までは補正できません。80%や90%といったしきい値は、正確な測定値ではなく「そろそろ空にして」という目安として扱ってください。警告がずっと鳴り続けないように、コードではヒステリシスを使っています。満杯の警告は90%で1回鳴り、ゴミ箱を空にして70%を下回るまで再び鳴りません。フィルタの違いはセンサーデータのフィルタリングの記事で詳しく説明しています。
電池での動作:どれくらい持つ?
ケーブルなしでスマートゴミ箱を動かしたくなりますが、Arduino Unoは電池向きのボードではありません。USBチップ、レギュレーター、電源LEDがあるため、何もしていなくても約45〜50 mAを消費します。2つのHC-SR04の待機電流、点灯しているLED 1個、解放したサーボの内部回路を加えると、平均電流は70〜80 mAほどになります。サーボが開閉中に流す100〜250 mAは数秒しか続かないので、1日50回開けるゴミ箱でも平均は約1 mA増えるだけです。電池の持ち時間はおおよそ容量(mAh)÷ 平均電流(mA)です。自分の値で計算するには電池の持ち時間ツールを使ってください。
| 電源 | 接続 | おおよその動作時間(75 mA) | 評価 |
|---|---|---|---|
| 9 V電池(006P / PP3、約500 mAh) | VIN | 5〜6時間 | 非推奨:サーボが動き出すと電圧が落ち、ボードがリセットされる |
| 単3アルカリ電池 × 6本(9 V) | DCジャックまたはVIN | 約1日 | 発表当日には十分 |
| 18650 × 2本(7.4 V、2500 mAh) | VIN。サーボ用にLM2596を5〜6 Vに設定 | 約1.5日 | 充電できる。2S用の充電器と保護基板が必要 |
| 10,000 mAhのモバイルバッテリー | USB | 3〜4日 | 50〜100 mA未満の負荷で電源が切れるモバイルバッテリーもある |
| 5 V 2 AのACアダプター | USBまたは5Vピン | 無制限 | 家で常時使うなら最適 |

表からわかる結論ははっきりしています。電池で動くUnoのゴミ箱は発表やデモには最適ですが、家で何か月も使うゴミ箱なら5 VのACアダプターを使いましょう。2本の18650とサーボの間に入れる降圧モジュールの設定方法はLM2596の記事で説明しています。電池の持ちを本気で延ばしたいなら、Unoの代わりに電源LEDを外したArduino Pro Miniを使い、測定の合間にプロセッサをスリープさせ、LEDはふたが開いたときだけ短く点灯させます。こうした対策で平均電流は15〜20 mAまで下がり、同じ電池が4〜5倍長持ちします。Arduinoの給電方法(USB、DCジャック、VIN、5Vピン)の違いはArduinoの電源のとり方の記事にあります。
ESP32でゴミの量を通知する(Telegram)
ゴミ箱がいっぱいかどうかをLEDで確かめるには、そばまで行く必要があります。一方、ESP32ならその情報をスマートフォンへ直接送れます。いちばん簡単なのはTelegramです。無料で、自分専用のボットを数分で作れるからです。Telegramで@BotFatherにメッセージを送り、/newbotコマンドでボットを作成して、表示されるトークンを控えます。次に、自分のボットに一度/startを送ります。ボットは、一度も話しかけてこなかった相手にはメッセージを送れません。自分のチャットIDを調べるには、@userinfobotのようなボットにメッセージを送るか、Bot APIのgetUpdatesリクエストを使います。コードに追加のライブラリは不要で、ESP32標準のHTTPClientでTelegramのsendMessageのアドレスにリクエストを送ります。
コードはUno版と同じステートマシンを使います。違うのは、サーボの信号をServoライブラリではなくESP32のPWMユニットであるLEDCで生成する点だけです。50 Hzの信号では、0.544〜2.4 msのパルスが0〜180度に対応し、パルスを止める(ledcWriteで0を書く)とサーボが解放されます。このコードはESP32 Arduinoコアのバージョン3.x向けです。バージョン2.xにはledcAttach()関数がないので、ボードマネージャでコアを更新してください。

1// ESP32 Arduino çekirdeği 3.x / ESP32 Arduino core 3.x
2#include <WiFi.h>
3#include <WiFiClientSecure.h>
4#include <HTTPClient.h>
5
6const char* WIFI_SSID = "WIFI_ADIN"; // kendi ağın / your network
7const char* WIFI_PASS = "WIFI_SIFREN";
8const char* BOT_TOKEN = "123456789:ABCdefGhIJKlmNoPQRstuVWxyz"; // BotFather'dan / from BotFather
9const char* CHAT_ID = "123456789"; // senin sohbet kimliğin / your chat ID
10
11// Pinler: ECHO'lar 1 kΩ / 2 kΩ bölücüyle / Pins: ECHOs via a 1 kΩ / 2 kΩ divider
12const int HAND_TRIG = 18, HAND_ECHO = 19;
13const int FILL_TRIG = 25, FILL_ECHO = 26;
14const int SERVO_PIN = 13;
15const int LED_GREEN = 27, LED_YELLOW = 32, LED_RED = 33;
16const int BUZZER = 23;
17
18const int HAND_CM = 20;
19const int LID_CLOSED = 10, LID_OPEN = 110;
20const unsigned long OPEN_TIME = 4000, CLOSE_STEP_MS = 20;
21const int EMPTY_CM = 35, FULL_CM = 8;
22const int NOTIFY_PCT = 85; // bu yüzdede bildir / notify at this percent
23const int RESET_PCT = 30; // bunun altına inince "boşaltıldı" / below this: "emptied"
24
25enum LidState { CLOSED, OPEN, CLOSING };
26LidState state = CLOSED;
27unsigned long lastHand = 0, lastHandPing = 0, closeStart = 0, closedAt = 0, lastFill = 0, lastTry = 0;
28int fillPercent = 0;
29byte handHits = 0;
30bool notified = false, servoOn = false, fullWarned = false;
31
32void servoWrite(int angle) { // 0–180° → 544–2400 µs, 50 Hz
33 uint32_t us = map(angle, 0, 180, 544, 2400);
34 ledcWrite(SERVO_PIN, us * 16384 / 20000); // 14 bit, 20 ms periyot / 14-bit, 20 ms period
35 servoOn = true;
36}
37
38void servoRelease() { // darbe yok: servo serbest kalır / no pulses: the servo relaxes
39 ledcWrite(SERVO_PIN, 0);
40 servoOn = false;
41}
42
43int pingCm(int trig, int echo) {
44 digitalWrite(trig, LOW); delayMicroseconds(2);
45 digitalWrite(trig, HIGH); delayMicroseconds(10);
46 digitalWrite(trig, LOW);
47 unsigned long t = pulseIn(echo, HIGH, 30000UL);
48 return t == 0 ? 999 : (int)(t / 58); // 999 = yankı yok / 999 = no echo
49}
50
51int medianCm(int trig, int echo) { // 5 ölçümün ortancası / median of 5 readings
52 int v[5];
53 delay(60); // önce el sensörünün yankısı sönsün / first let the hand sensor's echo die out
54 for (int i = 0; i < 5; i++) {
55 v[i] = pingCm(trig, echo);
56 delay(60);
57 }
58 for (int i = 0; i < 4; i++)
59 for (int j = i + 1; j < 5; j++)
60 if (v[j] < v[i]) { int tmp = v[i]; v[i] = v[j]; v[j] = tmp; }
61 return v[2];
62}
63
64int toPercent(int cm) {
65 long p = (long)(EMPTY_CM - cm) * 100 / (EMPTY_CM - FULL_CM);
66 return (int)constrain(p, 0L, 100L);
67}
68
69void showLevel() {
70 digitalWrite(LED_GREEN, fillPercent < 50);
71 digitalWrite(LED_YELLOW, fillPercent >= 50 && fillPercent < 80);
72 digitalWrite(LED_RED, fillPercent >= 80);
73}
74
75void beep(int n) {
76 for (int i = 0; i < n; i++) {
77 digitalWrite(BUZZER, HIGH); delay(120);
78 digitalWrite(BUZZER, LOW); delay(120);
79 }
80}
81
82String urlEncode(const String& s) { // Türkçe harfler ve boşluk için / for Turkish letters and spaces
83 const char* hex = "0123456789ABCDEF";
84 String out;
85 for (size_t i = 0; i < s.length(); i++) {
86 uint8_t c = s[i];
87 if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
88 out += (char)c;
89 } else {
90 out += '%';
91 out += hex[c >> 4];
92 out += hex[c & 15];
93 }
94 }
95 return out;
96}
97
98bool sendTelegram(const String& text) {
99 if (WiFi.status() != WL_CONNECTED) return false;
100 WiFiClientSecure client;
101 client.setInsecure(); // sertifika denetimi yok, hobi projesi için yeterli / no certificate check, fine for a hobby project
102 HTTPClient http;
103 String url = String("https://api.telegram.org/bot") + BOT_TOKEN +
104 "/sendMessage?chat_id=" + CHAT_ID + "&text=" + urlEncode(text);
105 if (!http.begin(client, url)) return false;
106 int code = http.GET(); // 200 = gönderildi / 200 = sent
107 Serial.printf("Telegram HTTP %d\n", code);
108 http.end();
109 return code == 200;
110}
111
112void openLid() {
113 servoWrite(LID_OPEN);
114 state = OPEN;
115 if (fillPercent >= 90) beep(2);
116}
117
118void setup() {
119 Serial.begin(115200);
120 pinMode(HAND_TRIG, OUTPUT); pinMode(HAND_ECHO, INPUT);
121 pinMode(FILL_TRIG, OUTPUT); pinMode(FILL_ECHO, INPUT);
122 pinMode(LED_GREEN, OUTPUT); pinMode(LED_YELLOW, OUTPUT); pinMode(LED_RED, OUTPUT);
123 pinMode(BUZZER, OUTPUT);
124 ledcAttach(SERVO_PIN, 50, 14); // 50 Hz, 14 bit servo sinyali / 50 Hz, 14-bit servo signal
125 servoWrite(LID_CLOSED);
126 delay(500);
127 servoRelease();
128 WiFi.mode(WIFI_STA);
129 WiFi.begin(WIFI_SSID, WIFI_PASS); // arka planda bağlanır; kutu Wi-Fi olmadan da çalışır / connects in the background; the bin works without Wi-Fi too
130 closedAt = millis();
131 showLevel();
132}
133
134void loop() {
135 unsigned long now = millis();
136
137 if (now - lastHandPing >= 60) { // el sensörü / hand sensor
138 lastHandPing = now;
139 int d = pingCm(HAND_TRIG, HAND_ECHO);
140 if (d >= 2 && d <= HAND_CM) {
141 if (handHits < 2) handHits++;
142 } else {
143 handHits = 0;
144 }
145 if (handHits >= 2) {
146 lastHand = now;
147 if (state != OPEN) openLid();
148 }
149 }
150
151 if (state == OPEN && now - lastHand >= OPEN_TIME) {
152 state = CLOSING;
153 closeStart = now;
154 }
155
156 if (state == CLOSING) { // yavaş kapanma / slow close
157 int moved = (now - closeStart) / CLOSE_STEP_MS;
158 int range = abs(LID_OPEN - LID_CLOSED);
159 if (moved >= range) {
160 servoWrite(LID_CLOSED);
161 state = CLOSED;
162 closedAt = now;
163 } else {
164 servoWrite(LID_OPEN > LID_CLOSED ? LID_OPEN - moved : LID_OPEN + moved);
165 }
166 }
167
168 if (state == CLOSED && servoOn && now - closedAt > 400) servoRelease();
169
170 if (state == CLOSED && now - closedAt > 1000 && now - lastFill >= 3000) {
171 lastFill = now;
172 int cm = medianCm(FILL_TRIG, FILL_ECHO);
173 if (cm < 999) fillPercent = toPercent(cm);
174 showLevel();
175 Serial.printf("Doluluk / Fill: %d cm = %%%d\n", cm, fillPercent);
176
177 if (fillPercent >= 90 && !fullWarned) { fullWarned = true; beep(3); }
178 if (fillPercent < 70) fullWarned = false;
179
180 // Bildirim: kapak kapalıyken, en fazla dakikada bir dene / Notify: only with the lid closed, retry at most once a minute
181 if (!notified && fillPercent >= NOTIFY_PCT && (lastTry == 0 || now - lastTry > 60000UL)) {
182 lastTry = now;
183 notified = sendTelegram("Çöp kutusu %" + String(fillPercent) + " dolu, boşaltma zamanı. / The bin is " +
184 String(fillPercent) + "% full.");
185 }
186 if (notified && fillPercent < RESET_PCT) { // boşaltıldı: yeniden kur / emptied: re-arm
187 notified = false;
188 lastTry = 0;
189 sendTelegram("Çöp kutusu boşaltıldı. / The bin has been emptied.");
190 }
191 }
192}通知はゴミ箱が85%に達したときに1回送られ、ゴミの量が30%を下回ると「空にしました」というメッセージとともに再び有効になります。そのため、スマートフォンが3秒ごとに震えることはありません。Wi-Fiがない場合や送信に失敗した場合、コードは1分に1回再試行し、その間もゴミ箱は普段どおりに動き続けます。暗号化された(TLS)接続のため、メッセージの送信には1〜3秒かかります。コードがふたの閉じているときにだけ送信するのはそのためです。setInsecure()はサーバーの証明書を検証せずに接続します。趣味のプロジェクトなら問題ありませんが、トークンは秘密の情報なので、コードを共有する前に必ず削除してください。シリアルモニタに「Telegram HTTP 200」と表示されればメッセージは送信されています。401はトークンの誤り、400はチャットIDの誤り、403はまだボットに/startを送っていないことを意味します。コマンドに応答する本格的なボットについては、ESP32のTelegramボットのガイドをご覧ください。
Raspberry Pi Picoで作るスマートゴミ箱(MicroPython)
Raspberry Pi Picoでは、同じロジックをMicroPythonで組みます。time_pulse_us()関数はArduinoのpulseIn()と同じように動きます。反響がないときは負の値を返すので、それを「反響なし」を意味する999として扱います。サーボには50 HzのPWMと、パルス幅をナノ秒で指定するduty_ns()を使います。duty_ns(0)でパルスが止まり、サーボが解放されます。時間の比較はticks_ms()とticks_diff()で行います。ticks_diffはカウンターが一周しても正しい差を返します。Thonnyでコードをmain.pyとしてPicoに保存すると、ボードの電源が入るたびに自動で実行されます。Pico Wを使っているなら、Pico WのWebサーバーの記事の方法を加えて、ゴミの量をWi-Fi経由で表示できます。
1from machine import Pin, PWM, time_pulse_us
2import time
3
4# Pinler: ECHO'lar 1 kΩ / 2 kΩ bölücüyle / Pins: ECHOs via a 1 kΩ / 2 kΩ divider
5hand_trig, hand_echo = Pin(2, Pin.OUT), Pin(3, Pin.IN)
6fill_trig, fill_echo = Pin(4, Pin.OUT), Pin(5, Pin.IN)
7green, yellow, red = Pin(10, Pin.OUT), Pin(11, Pin.OUT), Pin(12, Pin.OUT)
8buzzer = Pin(13, Pin.OUT)
9servo = PWM(Pin(15))
10servo.freq(50) # servo için 50 Hz / 50 Hz for the servo
11
12HAND_CM = 20
13LID_CLOSED, LID_OPEN = 10, 110
14OPEN_MS, CLOSE_STEP_MS = 4000, 20
15EMPTY_CM, FULL_CM = 35, 8
16
17def ping_cm(trig, echo):
18 trig.low(); time.sleep_us(2)
19 trig.high(); time.sleep_us(10) # 10 µs tetik / 10 µs trigger
20 trig.low()
21 t = time_pulse_us(echo, 1, 30000) # yankı yoksa negatif / negative if there is no echo
22 return 999 if t < 0 else t // 58
23
24def median_cm(trig, echo): # 5 ölçümün ortancası / median of 5 readings
25 v = []
26 time.sleep_ms(60) # önce el sensörünün yankısı sönsün / first let the hand sensor's echo die out
27 for _ in range(5):
28 v.append(ping_cm(trig, echo))
29 time.sleep_ms(60)
30 v.sort()
31 return v[2]
32
33def servo_write(angle): # 0–180° → 544–2400 µs
34 servo.duty_ns((544 + angle * 1856 // 180) * 1000)
35
36def servo_release(): # darbe yok: servo serbest / no pulses: servo relaxes
37 servo.duty_ns(0)
38
39def show_level(p):
40 green.value(p < 50)
41 yellow.value(50 <= p < 80)
42 red.value(p >= 80)
43
44def beep(n):
45 for _ in range(n):
46 buzzer.on(); time.sleep_ms(120)
47 buzzer.off(); time.sleep_ms(120)
48
49state = "closed"
50fill, hits, warned, released = 0, 0, False, False
51last_hand = last_ping = close_start = last_fill = time.ticks_ms()
52servo_write(LID_CLOSED)
53time.sleep_ms(500)
54servo_release()
55released = True
56closed_at = time.ticks_ms()
57show_level(fill)
58
59while True:
60 now = time.ticks_ms()
61
62 if time.ticks_diff(now, last_ping) >= 60: # el sensörü, 60 ms'de bir / hand sensor, every 60 ms
63 last_ping = now
64 d = ping_cm(hand_trig, hand_echo)
65 hits = min(hits + 1, 2) if 2 <= d <= HAND_CM else 0
66 if hits >= 2:
67 last_hand = now
68 if state != "open": # kapanırken de yeniden açar / also reopens while closing
69 servo_write(LID_OPEN)
70 state, released = "open", False
71 if fill >= 90:
72 beep(2)
73
74 if state == "open" and time.ticks_diff(now, last_hand) >= OPEN_MS:
75 state, close_start = "closing", now
76
77 if state == "closing": # yavaş kapanma / slow close
78 moved = time.ticks_diff(now, close_start) // CLOSE_STEP_MS
79 if moved >= abs(LID_OPEN - LID_CLOSED):
80 servo_write(LID_CLOSED)
81 state, closed_at = "closed", now
82 else:
83 servo_write(LID_OPEN - moved if LID_OPEN > LID_CLOSED else LID_OPEN + moved)
84
85 if state == "closed" and not released and time.ticks_diff(now, closed_at) > 400:
86 servo_release()
87 released = True
88
89 if state == "closed" and time.ticks_diff(now, closed_at) > 1000 and time.ticks_diff(now, last_fill) >= 3000:
90 last_fill = now
91 cm = median_cm(fill_trig, fill_echo)
92 if cm < 999: # yankı yoksa eski değer kalır / keep the old value if no echo
93 fill = max(0, min(100, (EMPTY_CM - cm) * 100 // (EMPTY_CM - FULL_CM)))
94 show_level(fill)
95 print("Doluluk / Fill:", cm, "cm =", fill, "%")
96 if fill >= 90 and not warned:
97 warned = True
98 beep(3)
99 if fill < 70:
100 warned = False
101
102 time.sleep_ms(5)学校の課題として発表するときのコツ
スマートゴミ箱は、TÜBİTAK 4006(トルコの学校科学フェア)のような科学コンテストや学校の展示会でとてもよく見かけるプロジェクトです。そのため審査員は、ふたが開け閉めすること自体よりも、どのように課題を定義し、どう測定し、何を結論づけたかに注目します。ありふれたデモを科学的な研究に変えるのは測定です。何回の試行で開いたか、どれくらい速く開いたか、ゴミの量をどれだけ正確に推定できたか、電池が何時間持ったか。発表は次の順番で組み立てられます。
- 課題:手でふたに触れると菌が広がり、あふれたゴミ箱はにおいや虫の原因になります。学校のゴミ箱のふたに休み時間1回の間に何人が触れるかを数え、実際のデータから始めましょう。
- 目的:触れずに開き、いっぱいになると警告し、必要に応じてスマートフォンにも通知するゴミ箱。
- しくみ:センサー、Arduino、サーボ、LED、ブザー、ESP32、スマートフォンを1枚のブロック図にまとめます。音速と「58で割る」式は1枚のスライドで説明します。
- 材料と費用:部品リストとサーボの選定を、トルクの計算と一緒に表にまとめて示します。
- 実演:手を近づけてふたを開けます。ゴミ箱に紙を入れていき、LEDが緑から黄、赤へ変わる様子を見せ、スマートフォンに届いたTelegramのメッセージをスクリーンに映します。
- テスト結果:下の表を自分の測定値で埋め、結果をグラフにします。
- 結論と改善点:うまくいったこと、いかなかったこと、次の版で変えたいことを正直に説明します。
| テスト | やり方 | 記録する値 |
|---|---|---|
| 検知距離 | 5、10、15、20、25 cmの位置にそれぞれ10回ずつ手をかざす | 各距離で何回ふたが開いたか(例:10/10) |
| 応答時間 | スマートフォンのスローモーション(240 fps)で撮影する | 手が現れてからふたが動き始めるまでの時間(ms) |
| 誤作動 | ゴミ箱の前を20回歩いて通り過ぎる | 意図しない開閉の回数 |
| ゴミの量の精度 | 定規で測りながら、紙で4分の1、半分、4分の3まで満たす | 実際のゴミの量と測定したパーセントの差 |
| 閉じる速さ | CLOSE_STEP_MSを5、20、40 msに設定する | 閉じるまでの時間と、スマートフォンで測った音の大きさ |
| 電池の持ち時間 | 新品の電池で動かし、1時間ごとに電圧を測る | ゴミ箱が何時間動いたか |
改良のアイデア
- ディスプレイ付きゴミ箱:OLEDディスプレイにゴミの量のパーセントと棒グラフを表示します。
- 音声でお礼:ふたが開くたびにDFPlayer Miniで短い「ありがとう」の音声を流します。
- 重さの測定:ゴミ箱の下にHX711とロードセルを置いてゴミが何キロあるかを測り、体積と重さを組み合わせます。
- 分別ゴミ箱:カラーセンサーと2つ目のサーボで、ゴミを2つの区画に振り分けます。
- においの警告:MQ-135でゴミ箱の中の空気が悪くなったことを検知します。
- 太陽光で動く屋外ゴミ箱:小さなソーラーパネル、18650電池1本、TP4056充電モジュール、5 Vの昇圧モジュールで、ケーブルを完全になくします。
- ゴミの量の記録:ESP32で読み取り値をGoogle Sheetsに書き込み、どの曜日にいちばん早くいっぱいになるかをグラフにします。
- 学校全体や団地全体で:Wi-Fiが届かない敷地内のゴミ箱を、LoRaモジュールで数百メートル離れた場所から見守ります。
よくある失敗と対策
| 症状 | 考えられる原因 | 対策 |
|---|---|---|
| ふたが勝手に開閉する | センサーが近くの壁や通り過ぎる人を検知している | HAND_CMを小さくし、センサーを上向きに傾け、前方30 cmをあける |
| ふたが開くとArduinoが再起動する | サーボの起動電流が5 Vのラインを引き下げている | 470 µFのコンデンサ、強いサーボは別電源、GNDを共通に |
| ふたが閉じているときにサーボがジーッと鳴って熱くなる | LID_CLOSEDでふたを縁に押し付けている | 角度を2〜3度戻す。detach()の行は消さない |
| ふたが途中で止まる、または震える | トルク不足、または軸がずれている | より強いサーボ、軽いふた、カウンターウェイト。サーボの軸をヒンジにそろえる |
| 空なのにゴミの量が多く出る | 袋のふちがセンサーの前にある、または狭いゴミ箱で壁の反響を拾っている | 袋を外側に折り返し、センサーを中央にして、EMPTY_CMを測り直す |
| ゴミの量がまったく変わらない | センサーに反響が届かない(999)か、ECHOの線が外れている | シリアルモニタで生のcm値を見て、配線を確認する |
| ESP32で距離が常に999かでたらめな値になる | ECHOの分圧回路がない、または間違っている、あるいは線が入れ替わっている | 1 kΩ / 2 kΩの分圧回路とTRIG・ECHOの順番を確認する |
| ESP32が何度も再起動する | Wi-Fiとサーボが同時に電流を消費している、USBケーブルが弱い | 短くて品質のよいケーブルを使い、サーボは別の5 V電源から給電する |
| Telegramのメッセージが届かない | トークンかチャットIDの誤り、ボットに/startを送っていない | シリアルモニタでHTTPコード(401、400、403)を確認する |
スマートゴミ箱は、センサーの読み取り、サーボの制御、時間管理、電源の計画を1つのプロジェクトにまとめているので、Arduinoを学ぶ最も楽しい方法の1つです。一度作ってしまえば、同じ部品で駐車センサーや自動ドアを作るのもずっと簡単になります。BlueGraysアプリでは、HC-SR04での距離測定、サーボの制御、ブザーの使い方、ESP32の駐車センサー、Telegram通知付きの動体アラームなどのプロジェクトを、回路図と完成したコード付きでステップごとに紹介しています。
よくある質問
Arduinoのスマートゴミ箱には何が必要ですか?
基本版なら、Arduino UnoまたはNano、HC-SR04超音波センサー1個、ふたの重さに合ったサーボ(軽いふたならSG90、本物のゴミ箱のふたならMG996R)、ジャンパーワイヤー、5 Vの電源があれば十分です。ゴミの量を測るには、2つ目のHC-SR04、220 Ωの抵抗付きのLED 3個、アクティブブザーを加えます。スマートフォンに通知したい場合は、Arduinoの代わりにESP32を使えます。
スマートゴミ箱はどのようなしくみで動きますか?
ゴミ箱の前面にある超音波センサーが音波を出し、反響が戻るまでの時間を測ります。20 cmより近くに手を検知すると、Arduinoがサーボモーターでふたを開けます。手が離れて数秒たつと、ふたはゆっくり閉まります。ふたの裏にある2つ目のセンサーがゴミの高さを測り、LEDとブザーでゴミの量を知らせます。
サーボでゴミ箱のふたが持ち上がらないのはなぜですか?
たいていは、ふたに対してサーボのトルクが足りないのが原因です。必要なトルクは、ふたの重さにヒンジから重心までの距離を掛けた値です。120 g、25 cmのふたなら約1.5 kg·cmが必要で、これはSG90の限界にあたります。計算値の2倍の定格を持つサーボを選ぶか、ふたを軽くするかカウンターウェイトを付け、サーボの電源が十分な電流を供給できるかも確認してください。
スマートゴミ箱でHC-SR04の代わりに赤外線センサーを使えますか?
はい。FC-51のような赤外線障害物センサーなら2〜30 cmで手を検知でき、何かがあるかないかだけを返すのでコードはさらに簡単になります。ただし日光の影響を受け、暗い色の面は検知が遅れます。PIRセンサーは前を通る人すべてに反応するので、ゴミ箱のふたには向きません。
Arduinoのスマートゴミ箱は電池でどれくらい動きますか?
Arduino Unoを使ったゴミ箱は平均70〜80 mAを消費するので、単3電池6本で約1日、18650電池2本で約1日半、10,000 mAhのモバイルバッテリーで3〜4日動きます。常時使うなら5 VのACアダプターがおすすめです。Pro Mini、スリープモード、必要なときだけLEDを点灯させる工夫を組み合わせると、電池が4〜5倍長持ちします。
ゴミ箱がいっぱいになったらスマートフォンに通知するには?
ESP32がWi-Fiに接続し、ゴミの量が設定したパーセントを超えると、Telegram Bot APIにリクエストを送ってスマートフォンにメッセージを届けます。そのためには、BotFatherから受け取ったボットのトークンと自分のチャットIDが必要です。同じ通知が何度も送られないように、コードはゴミ箱が空になるまで待ちます。
この記事を読んだ後に作れるプロジェクト
全プロジェクトを見る
HC-SR04で距離を測定(シリアルモニタ表示)
HC-SR04センサーは、コウモリが暗闇で道を見つけるのと同じ仕組みで動作します。人には聞こえない音を出し、その反響を捉えて前方の物体までの距離を測ります。Arduinoは0.5秒ごとに距離をセンチメートル単位でコンピューターの画面に表示し、3〜100センチメートルの測定値を示します。駐車センサーや障害物を避けるロボットなどのプロジェクトの基礎になります。

ブザーの使い方
呼び鈴やアラームがどうやって音を出すのか、気になったことはありませんか?Arduinoが小さなブザーを短い間隔で鳴らし、音を出す、止める、を繰り返します。電気で音が生まれる仕組みを学び、メロディーを演奏するプロジェクトへの第一歩を踏み出せます。

サーボモーターの使い方
小型サーボモーターのアームが、一方の端、中央、もう一方の端の3つの位置へ順番に回り、それぞれの位置で2秒間止まります。Arduinoが保持する角度をモーターに指示すると、モーターはその位置まで動いて止まります。ロボットアームや自動開閉するふたが動く仕組みを学べます。

HC-SR04で作る駐車センサー
車の駐車センサーを小さく再現します。物体が近づくほど、ビープ音の間隔が短くなります。距離センサーが前方の物体までの距離を測り、20センチメートルより遠ければ緑のLEDが点灯します。近づくと赤いLEDが点滅してブザーが鳴り、さらに近づくほど警告の間隔が短くなります。車が壁にぶつからずに駐車できる仕組みを、机の上で確かめられます。

ESP32とHC-SR04で作る超音波距離計
HC-SR04超音波センサーは、発した音波が物体に反射して戻るまでの時間(Time of Flight)を測定して距離を計算します。このプロジェクトでは、ESP32からセンサーのTRIGピンに10マイクロ秒のパルスを送り、ECHOピンからの反射波の持続時間をpulseIn()で読み取ります。計算した距離をcm単位で16x2 I2C LCDに表示します。HC-SR04は5Vで動作するため、ECHOピンには分圧回路が必須です。分圧しないとESP32の3.3V GPIOが破損します。

ESP32サーボモーター(ポテンショメーター制御)
このプロジェクトでは、ポテンショメーターでSG90サーボモーターを制御します。ポテンショメーターから読み取ったアナログ値を0~180度の範囲に変換し、サーボの角度を調整します。ESP32Servoライブラリは、ESP32の出力に対応した適切なGPIOでサーボ信号を生成します。このプロジェクトではGPIO 18を使います。ロボットアームやカメラ用ジンバル、自動ドアの機構に使われる基本的な制御方法です。

ESP32駐車センサー(HC-SR04 + 5連LEDバー + ブザー + OLED)
車の駐車センサーをESP32で作ります!HC-SR04超音波センサーで前方の物体までの距離を測ります。距離が縮まるほど5連LEDバーが緑 → 黄 → 赤の順に点灯し、ブザーの鳴る間隔が短くなります。10 cm未満まで近づくとアラームが鳴り続け、すべてのLEDが点灯します。SSD1306 OLEDには現在の距離をcm単位の大きな文字で表示し、状態メッセージ(落ち着いて / 注意 / 接近 / 停止! / 危険)とLEDバーのシミュレーションも表示します。HC-SR04のpulseIn、複数のデジタル出力、ブザーを学べる定番のプロジェクトです。

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

