ガイド · 読了まで21分

Arduinoで作るスマートゴミ箱:HC-SR04、サーボのふた、満杯アラーム

Arduinoでスマートゴミ箱を作ります。HC-SR04での手の検知、ゆっくり閉まるサーボのふた、ゴミの量を示すLEDとブザー、電池の計画、ESP32からのTelegram通知まで解説します。

Arduinoで作るスマートゴミ箱は、ふたに触れずに開けられるシンプルながら学びの多い自動化プロジェクトで、学校の課題としても特に人気があります。ゴミ箱の前面に付けたHC-SR04超音波センサーが近づいてくる手を検知し、Arduinoがサーボモーターでふたを開けます。手を離して数秒たつと、ふたをバタンと落とさずにゆっくり閉じます。ふたの裏に付けた2つ目のHC-SR04はゴミの高さを測ります。ゴミがたまるにつれて緑、黄、赤のLEDが順に点灯し、完全にいっぱいになるとブザーが鳴ります。

このガイドでは部品リストから始めて、センサーを取り付ける位置、サーボのトルクの計算方法、ふたをヒンジ(蝶番)に取り付ける方法、キャリブレーション、そして待ち時間とゆっくり閉じる動作を同時に扱う完全なコードを順を追って説明します。続いて、電池でプロジェクトが実際にどれくらい動くかを計算し、ESP32でゴミの量の通知をTelegramのメッセージとしてスマートフォンに送り、同じプロジェクトをRaspberry Pi PicoとMicroPythonで書きます。最後には、学校の課題の発表で使える構成案と、自分の測定値で埋められるテスト表もあります。

HC-SR04超音波距離センサー
スマートゴミ箱ではHC-SR04が2つの役割を担います。前面のセンサーは近づく手を、ふたの裏のセンサーはゴミの高さを測ります。2〜400 cmの範囲を狭い角度で検知します。

スマートゴミ箱のしくみ

このプロジェクトは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が必要
Arduino Uno
Arduino Uno
SG90サーボモーター
SG90サーボモーター
ブザー
ブザー
LED
LED

手の検知にはどのセンサーを使う?

ゴミ箱を開けるセンサーはいくつかありますが、どれもこの用途に同じように向いているわけではありません。本当に必要なのは、ゴミ箱から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より狭いゴミ箱では、音が側面で反射して誤った値になることがあります。ゴミ袋の口はゴミ箱の外側に折り返してください。内側に垂れた袋のふちがセンサーの前に入ると、空でもいっぱいに見えてしまいます。

2つのHC-SR04を同時にトリガーしてはいけません。それぞれが相手の反響を自分のものと受け取り、どちらもでたらめな値を読んでしまいます。コードでは2つのセンサーを少なくとも60 msの間隔をあけて順番に測定し、ゴミ量センサーが5回測定している間は手検知センサーが待機します。センサーの前面もふさがないようにします。ゴミ箱にあけた穴のふちはセンサーの面とそろえ、薄いプラスチックや透明テープが1枚あるだけでも反響はさえぎられます。

配線図: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 UnoESP32Raspberry Pi Pico
手検知センサーのTRIGD2GPIO 18GP2
手検知センサーのECHOD3(直結)GPIO 19、1 kΩ / 2 kΩの分圧回路経由GP3、1 kΩ / 2 kΩの分圧回路経由
ゴミ量センサーのTRIGD4GPIO 25GP4
ゴミ量センサーのECHOD5(直結)GPIO 26、分圧回路経由GP5、分圧回路経由
HC-SR04のVCC(両方)5V5V(VIN)VBUS
サーボの信号(オレンジ)D9GPIO 13GP15
サーボの+(赤)5V(SG90)または外部5〜6 V5V(VIN)または外部5〜6 VVBUSまたは外部5〜6 V
緑、黄、赤のLED(220 Ω付き)D10、D11、D12GPIO 27、32、33GP10、GP11、GP12
アクティブブザー(+)D8GPIO 23GP13
すべてのGNDGNDGNDGND
HC-SR04のECHOピンは5 Vのパルスを出力しますが、ESP32とPicoのピンは3.3 Vで、5 Vトレラントではありません。各センサーのECHO線にそれぞれ分圧回路を組みます。ECHO → 1 kΩ → ボードへつなぐ点 → 2 kΩ → GNDの順です。これで5 V × 2 / 3 ≈ 3.3 Vになります。TRIGには分圧回路は不要で、3.3 Vのトリガーでもセンサーは十分に反応します。ESP32の5V(VIN)ピンとPicoのVBUSピンから5 Vが出るのは、ボードがUSBで給電されているときだけです。

サーボの選び方とトルクの計算

スマートゴミ箱のプロジェクトで最も多いトラブルは、ふたが途中まで開いて震える、あるいはまったく持ち上がらないというものです。原因はほぼ必ず、ふたに対してサーボが非力すぎることです。必要なトルクの見積もりは簡単です。トルク(kg·cm)=ふたの重さ(kg)× ヒンジから重心までの距離(cm)。厚さが均一な平らなふたなら、重心はちょうど中央にあります。長さ24 cmのふたなら、その距離は12 cmです。いちばん負荷が大きいのは、ふたが水平な開き始めの瞬間です。ふたが持ち上がるにつれて必要なトルクは減り、垂直になるとほぼゼロになります。サーボのカタログに載っているトルクは停動トルク(ストールトルク)、つまりサーボが力んで止まってしまう点の値です。この限界近くで動かし続けるとサーボは発熱し、ギアがすり減ります。そのため、計算結果を少なくとも2倍にしてください。

ふた重さ重心必要なトルク2倍の余裕込みおすすめ
20 × 20 cmの段ボールまたはスチレンボード50 g10 cm0.5 kg·cm1.0 kg·cmSG90
25 cmのプラスチック製ゴミ箱のふた120 g12.5 cm1.5 kg·cm3.0 kg·cmMG996R(MG90Sはぎりぎり)
30 cmのMDFまたは合板のふた300 g15 cm4.5 kg·cm9.0 kg·cmMG996R+カウンターウェイト
サーボトルク(4.8 V / 6 V)ギア電流メモ
SG901.8 kg·cm(4.8 Vの値のみ公表)プラスチック動作中100〜250 mAArduinoの5Vピンから給電できる
MG90S1.8 / 2.2 kg·cm金属動作中150〜300 mASG90と同じサイズで、より丈夫
MG996R9.4 / 11 kg·cm金属動作中0.5〜0.9 A、停動時は最大2.5 A別の5〜6 V電源から給電する必要がある
MG996R金属ギアのサーボモーター
MG996R:6 Vで約11 kg·cmのトルクを持つ金属ギアのサーボです。本物のゴミ箱のプラスチック製のふたも楽に持ち上げますが、停動時に最大2.5 Aを流すことがあるため、Arduinoからではなく別の電源から給電する必要があります。

より強いサーボを買う代わりに、ふたのバランスをとるだけで解決することもよくあります。ふたにヒンジの後ろへ伸びる短い腕があれば、そこにボルトや硬貨をいくつか貼り付けると重さの一部がつり合います。このカウンターウェイトで、必要なトルクを最大で半分まで減らせます。サーボホーンでヒンジから離れた位置を押す方法もありますが、てこでトルクがただで得られるわけではありません。ヒンジから離れるほど必要な力は小さくなりますが、押す点の移動距離が長くなるので長いサーボホーンが必要になり、長いホーンはサーボに求めるトルクを再び大きくします。ふたを90度近くまで開けたいなら、必要なサーボのトルクはヒンジに直接取り付けた場合とほとんど変わりません。サーボの種類の詳しい比較はサーボモーターとは?とMG996Rサーボの記事にあります。

取り付けのコツ:ヒンジ、サーボホーン、ケーブル

  • 重さはヒンジで支える:ふたは本物のヒンジ(ゴミ箱に付いているヒンジか、家具用の小さな蝶番)に取り付けます。ふたをサーボの軸だけで支えると、重さがすべてサーボの軸受けにかかり、SG90のプラスチックギアはすぐに壊れてしまいます。
  • サーボの軸をヒンジの軸にそろえる:いちばん簡単なのは、サーボの軸がヒンジの線の延長上に来るように、サーボをゴミ箱の後ろのふちにねじで固定し、サーボホーンをねじかホットボンドでふたに留める方法です。軸がずれていると、開くたびにサーボがふたを横方向に押してしまいます。
  • サーボ本体をしっかり固定する:サーボを両面テープだけで留めないでください。ふたを持ち上げるときに本体がねじれてしまいます。サーボの取り付け穴に2本のねじを通すか、小さなL字金具を使います。
  • ふたを縁に押し付けない:閉じた角度は、ふたが縁に載っていてもサーボが押していない位置に設定します。閉じたふたをサーボが押し続けると、ジーッと音を立てて発熱し、電流を無駄にします。
  • ケーブルに余裕を持たせる:ゴミ量センサーの配線はふたと一緒に動きます。ケーブルはヒンジの近くを通し、ふたを全開にしても引っ張られない長さにして、結束バンドで固定します。
  • 電子部品をゴミから離す:Arduino、電池、ブレッドボードは、ゴミ箱の背面か底に貼り付けた小さな箱に入れます。ゴミ袋から漏れた液体や湿気が回路に届かないようにしてください。
  • まず段ボールの模型で作る:学校の課題なら、靴箱やスチレンボードで作った軽いふたはSG90でも問題なく動きます。発表でトルクの計算を見せる例としてもぴったりです。

セットアップとキャリブレーションの手順

メインのコードに進む前に、下の小さなキャリブレーション用スケッチを書き込みます。このスケッチは、シリアルモニタに入力した角度へサーボを動かし、2つのセンサーが読んだ距離を0.5秒ごとに表示します。これで、自分のゴミ箱に合わせて、ふたの閉じた角度と開いた角度、空のゴミ箱の深さ、手を検知するしきい値を測れます。

C++ (Arduino IDE)
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}
  1. サーボホーンを付けずにコードを書き込み、シリアルモニタを9600ボーで開きます。改行の設定はLFのみ(Newline)にします。サーボは90度の位置に動きます。
  2. 10と入力してEnterキーを押します。ふたを閉じた状態で押さえ、その位置でサーボホーンを軸にはめてから、ふたに固定します。
  3. 角度を20、40、60と増やしていき、ふたが完全に開く値を探します。たいていは90〜110度です。ふたを垂直より先まで開けないでください。その値がLID_OPENになります。サーボが逆方向に回る場合は、閉じた値と開いた値をそれに合わせて選びます。コードはどちらの方向でも動きます。
  4. 小さな角度に戻ります。ふたが縁にぴったり載る角度から、開く方向へ2〜3度戻した値がLID_CLOSEDになります。閉じているときにサーボがジーッと鳴るなら、ふたを押し付けています。
  5. ゴミ袋をセットした空の状態で、ゴミ量センサーが読む値を記録します。これがEMPTY_CMです。FULL_CMには6〜10 cmを選びます。ゴミがこの高さに達しても、ふたは楽に閉まり、センサーも確実に測定できなければなりません。HC-SR04は2〜3 cmより近い距離を測れないからです。
  6. 手検知センサーの前に5、10、20、30 cmの位置で手をかざし、読み取り値を見ます。次にゴミ箱の前を歩いて通り過ぎ、読み取られた最も近い値を記録します。HAND_CMはその値より小さくする必要があります。たいていのゴミ箱では15〜25 cmがうまくいきます。
  7. 求めた値をメインのコードの先頭にある設定に入力し、書き込みます。

メインのコード:手の検知、待ち時間、ゆっくり閉じる動作

メインのコードには長いdelay()がなく、すべてmillis()で記録した時刻を使って処理します。そのため、ふたが閉じている途中でも手検知センサーは読み取りを続け、手が再び近づけばふたはすぐに開き直します。子どもの手がふたに挟まるのを防ぐ、シンプルですが大切な安全機能です。止めない時間管理の詳細はmillis()とdelay()で、状態で考える方法はArduinoのステートマシンの記事で紹介しています。

C++ (Arduino IDE)
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%を下回るまで再び鳴りません。フィルタの違いはセンサーデータのフィルタリングの記事で詳しく説明しています。

音速は温度によって変わり、おおよそv = 331.3 + 0.606 × T(m/s)です。58という数値は20 °Cのときに正しい値で、5 °Cのベランダでは音が遅くなるため、測定値は約3%長めに出ます。深さ35 cmのゴミ箱なら誤差は1 cm未満で、ゴミの量には影響しませんが、審査員に聞かれたときに理由を説明できれば好印象です。

電池での動作:どれくらい持つ?

ケーブルなしでスマートゴミ箱を動かしたくなりますが、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)VIN5〜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のモバイルバッテリーUSB3〜4日50〜100 mA未満の負荷で電源が切れるモバイルバッテリーもある
5 V 2 AのACアダプターUSBまたは5Vピン無制限家で常時使うなら最適
18650リチウムイオン電池ホルダー
18650電池ホルダー:2本を直列にすると7.4 Vになります。この電圧はVIN経由でArduinoに給電するのに適しています。サーボには、5〜6 Vに設定した降圧モジュールを間に入れます。

表からわかる結論ははっきりしています。電池で動くUnoのゴミ箱は発表やデモには最適ですが、家で何か月も使うゴミ箱なら5 VのACアダプターを使いましょう。2本の18650とサーボの間に入れる降圧モジュールの設定方法はLM2596の記事で説明しています。電池の持ちを本気で延ばしたいなら、Unoの代わりに電源LEDを外したArduino Pro Miniを使い、測定の合間にプロセッサをスリープさせ、LEDはふたが開いたときだけ短く点灯させます。こうした対策で平均電流は15〜20 mAまで下がり、同じ電池が4〜5倍長持ちします。Arduinoの給電方法(USB、DCジャック、VIN、5Vピン)の違いはArduinoの電源のとり方の記事にあります。

サーボ、センサー、ArduinoのGNDはすべて共通にする必要があります。別の電源から給電しているサーボのGNDをArduinoにつながないと、サーボは信号を正しく読み取れず、ぶるぶる震えます。サーボの電源線の間には470 µFのコンデンサを入れます。ふたが開くたびにArduinoが再起動するなら、サーボが動き出すときに5 Vのラインを引き下げています。原因と対策は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()関数がないので、ボードマネージャでコアを更新してください。

ESP32開発ボード
ESP32:Wi-Fiを内蔵しているので、ゴミ箱がいっぱいになるとスマートフォンにTelegramのメッセージを送ります。HC-SR04のECHO線は、3.3 Vに分圧せずにピンへつないではいけません。
C++ (Arduino IDE)
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経由で表示できます。

MicroPython
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. 課題:手でふたに触れると菌が広がり、あふれたゴミ箱はにおいや虫の原因になります。学校のゴミ箱のふたに休み時間1回の間に何人が触れるかを数え、実際のデータから始めましょう。
  2. 目的:触れずに開き、いっぱいになると警告し、必要に応じてスマートフォンにも通知するゴミ箱。
  3. しくみ:センサー、Arduino、サーボ、LED、ブザー、ESP32、スマートフォンを1枚のブロック図にまとめます。音速と「58で割る」式は1枚のスライドで説明します。
  4. 材料と費用:部品リストとサーボの選定を、トルクの計算と一緒に表にまとめて示します。
  5. 実演:手を近づけてふたを開けます。ゴミ箱に紙を入れていき、LEDが緑から黄、赤へ変わる様子を見せ、スマートフォンに届いたTelegramのメッセージをスクリーンに映します。
  6. テスト結果:下の表を自分の測定値で埋め、結果をグラフにします。
  7. 結論と改善点:うまくいったこと、いかなかったこと、次の版で変えたいことを正直に説明します。
テストやり方記録する値
検知距離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時間ごとに電圧を測るゴミ箱が何時間動いたか
審査員はたいてい弱点について質問するので、答えを事前に準備しておきましょう。HC-SR04はやわらかく凹凸の多い面をうまく検知できないので、厚手の布の手袋ではふたが遅れて開くことがあります。ゴミの量の測定は、平均ではなくゴミのいちばん高い点をとらえます。Unoを使ったゴミ箱は電池だと1〜2日しか持ちません。こうした限界をわかっていることを示し、それぞれに改善策を提案するほうが、プロジェクトを完璧なものとして見せるよりもずっと高く評価されます。

改良のアイデア

  • ディスプレイ付きゴミ箱: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つずつ確かめます。まずキャリブレーション用スケッチで、2つのセンサーがシリアルモニタにまともな値を出すかを確認し、次にサーボが閉じた角度と開いた角度へなめらかに動くかを確かめ、それからメインのコードを書き込みます。ESP32で「Brownout detector was triggered」(ブラウンアウト検出)というメッセージが出る場合、原因と対策はESP32が何度も再起動するの記事にあります。

スマートゴミ箱は、センサーの読み取り、サーボの制御、時間管理、電源の計画を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で距離を測定(シリアルモニタ表示)
#27初級

HC-SR04で距離を測定(シリアルモニタ表示)

HC-SR04センサーは、コウモリが暗闇で道を見つけるのと同じ仕組みで動作します。人には聞こえない音を出し、その反響を捉えて前方の物体までの距離を測ります。Arduinoは0.5秒ごとに距離をセンチメートル単位でコンピューターの画面に表示し、3〜100センチメートルの測定値を示します。駐車センサーや障害物を避けるロボットなどのプロジェクトの基礎になります。

4.6(46)
71
ブザーの使い方
#13初級

ブザーの使い方

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

4.7(89)
161
サーボモーターの使い方
#35初級
プレミアム

サーボモーターの使い方

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

4.7(48)
67
HC-SR04で作る駐車センサー
#28中級
プレミアム

HC-SR04で作る駐車センサー

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

4.8(64)
85
ESP32とHC-SR04で作る超音波距離計
#116中級

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が破損します。

4.9(28)
45
ESP32サーボモーター(ポテンショメーター制御)
#118中級
プレミアム

ESP32サーボモーター(ポテンショメーター制御)

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

4.3(29)
42
ESP32駐車センサー(HC-SR04 + 5連LEDバー + ブザー + OLED)
#142上級
プレミアム

ESP32駐車センサー(HC-SR04 + 5連LEDバー + ブザー + OLED)

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

0.0(0)
14
ESP32動体検知アラーム(PIR + ブザー + LED + Telegram)
#145上級
プレミアム

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

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

0.0(0)
9