Arduinoで作るスマート白杖:障害物・水たまり・転倒を知らせる
Arduinoでスマート白杖を作る方法。HC-SR04による障害物検知、振動での通知、水センサー、MPU6050による転倒検知、SOS、完全なコードを解説します。
Arduinoのスマート白杖は、白杖では見つけられない障害物を視覚障害のある人に知らせる装置で、学校の課題や科学コンテストで特に人気のある電子工作プロジェクトの一つです。持ち手の近くに取り付けたHC-SR04超音波センサーが前方にあるものまでの距離を測ります。障害物が近づくほど持ち手の振動モーターが速く強く震え、必要ならブザーも振動のたびに短いビープ音を鳴らします。これで、杖の先端が決して触れない腰より上の障害物を、数歩手前で感じ取れるようになります。
このガイドでは基本の回路から始め、プロジェクトを本当に使えるものにする追加機能をステップごとに加えていきます。白杖の先端近くの水たまりセンサー、MPU6050で白杖が倒れたことに気づき、20秒以内に誰もキャンセルしなければSOSを鳴らすアルゴリズム、18650セル1本で25時間以上動く電源回路、重さと持ちやすさを考えたケース、そして位置情報をSMSで送るESP32+GPSの拡張です。最後には、プロジェクトのテスト方法と科学コンテストでの発表方法についての具体的なコツも紹介します。

スマート白杖とは?何をするもの?
白杖は、視覚障害のある人がいちばん頼りにしている道具です。杖でたたくたびに、路面、縁石、段差、前方の低い障害物がわかります。しかし白杖が「見る」ことができるのは、先端が触れたものだけです。低く張り出した木の枝、歩道の上に突き出た看板、トラックのサイドミラー、開いた窓、壁から飛び出したエアコンの室外機など、腰より上の障害物は先端に触れることがなく、代わりに体や頭にぶつかります。スマート白杖は、このすき間を埋めるために設計されています。電子センサーが白杖の届かない高さと距離を調べ、その結果を振動として手に伝えます。
最初に一つはっきりさせておきましょう。スマート白杖は白杖や歩行訓練の代わりになるものではなく、それを補うものです。超音波センサーには死角があり、電池は切れます。そのため設計の鉄則は、電子回路が完全にオフになっても白杖としては以前とまったく同じように使えることです。毎年10月15日の「国際白杖の日」(White Cane Safety Day)は、このプロジェクトを学校で披露する絶好の機会です。
スマート白杖の仕組みは?
このプロジェクトは、1秒間に数百回まわる1つのループでできていて、4つの独立した仕事を同時にこなします。60ミリ秒ごとにArduinoは距離を測り、直近3回の測定値の中央値をとって、それに合わせて振動の間隔を調整します。0.5秒ごとに白杖の先端の水センサーを読みます。1秒間に50回MPU6050の加速度の値を読んで、白杖が倒れていないかを確認します。そして30秒ごとに電池の電圧を確認します。出力は役割の異なる2つのチャンネルに分かれています。振動は障害物専用で、音は水、転倒、SOS、電池残量の低下といった特別な出来事を知らせます。ボタンを短く押すと、必要に応じて障害物のビープ音をオン・オフできます。
| 部品 | 役割 | メモ |
|---|---|---|
| Arduino Nano(試作にはUno) | 測定と判断を行い、モーターとブザーを動かす | Nanoは小さくて軽いので白杖に向いている |
| HC-SR04超音波センサー | 前方の障害物までの距離を測る | 5 V、約15 mA。雨の中で使うならJSN-SR04T |
| 振動モーターモジュール | 障害物を振動で手に知らせる | トランジスタとダイオード付きの完成品モジュール |
| パッシブブザー | 水、転倒、SOS、電池のアラート | パッシブなので異なる音程を鳴らせる |
| 水位センサー | 白杖の先端が水に入ると知らせる | 先端から1〜2 cm上、測定中だけ電源を入れる |
| MPU6050(GY-521) | 白杖が倒れて横になったことを検知する | I2C、アドレス0x68 |
| 押しボタン | 音のオン・オフ、アラームの解除、長押しでSOS | 親指の下に配置 |
| 18650セル+TP4056+MT3608 | 持ち運べる電源、USB充電、5 Vへの昇圧 | 保護回路付き(DW01搭載)のTP4056を使う |
| 100 kΩの抵抗×2 | 電池電圧を測るための分圧回路 | 中点をA0へ |
| スイッチとケース | 電源のオン・オフと保護 | 3Dプリントか小さな工作用ケース |




障害物の検知:HC-SR04はどう測り、どこに付けるのか
HC-SR04はTRIGピンに10マイクロ秒のパルスを受けると、40 kHzの音波を8回送り、反響が戻るまでECHOピンをHIGHに保ちます。20 °Cでは音は1秒間に約343メートル進むので、往復を考えると1センチメートルごとに約58マイクロ秒かかります。つまり距離は単純にcm = 時間(µs)/ 58です。センサーが見えるのは約15度の円すいの中で、1.5メートル先ではこの円すいの幅は約40 cmになります。白杖を左右に振るとセンサーも一緒に向きを変えるので、前方の幅約1メートルの帯を自動的に調べることになります。センサーの詳しい説明はHC-SR04距離センサーの記事にあります。
取り付けでいちばん重要なのは角度です。歩くとき、白杖はまっすぐ立てて持つのではなく、垂直から約40〜50度前に傾けて持ちます。センサーを白杖と平行に貼り付けると地面を向いてしまい、床をずっと障害物と勘違いします。ケースは持ち手の近く、地面から約75 cmの位置に取り付け、センサーは水平より少し上に向けます。白杖を45度傾けたとき、センサーが水平より15度上を向くには、センサーの軸とシャフトのなす角を(先端側に測って)60度にする必要があります。正しい角度を見つけるいちばん簡単な方法は、白杖をいつもの持ち方で構え、スマホの水準器アプリをケースの前面に当てることです。
| センサーの角度(水平より上) | 1.5 m先で調べる高さ | とらえる代表的な障害物 |
|---|---|---|
| 0° | 約55〜95 cm | ベンチ、ゴミ箱、駐車中の車の車体 |
| 15° | 約95〜135 cm | 人の胴体、自転車のハンドル、トラックの荷台 |
| 25° | 約120〜170 cm | 看板、木の枝、開いた窓ガラス、サイドミラー |
センサー1つで始めるなら、15〜20度がちょうどよい妥協点です。体の高さの障害物を無理なくとらえられます。ただし頭の高さの障害物は、まだ遠くにあるあいだだけ円すいの上端に入り、近づくにつれてそこから外れてしまいます。頭の高さが重要なら、上に30〜35度傾けた2つ目のHC-SR04を専用に使いましょう。このセンサーは斜めに測るので、範囲は200 cm程度にしておきます。2つのセンサーは少なくとも60 msの間隔をあけて順番にトリガーすることを忘れないでください。そうしないと、片方がもう片方の反響を拾ってしまいます。レーザー(ToF)や赤外線センサーとの詳しい比較は、距離センサーの比較の記事を参照してください。
| センサー | 範囲 | 長所 | 短所 |
|---|---|---|---|
| HC-SR04 | 2〜400 cm | 安い、簡単、円すいが広い | 防水ではない、柔らかい面が苦手 |
| JSN-SR04T | 約25〜450 cm | 防水プローブで雨の中でも使える | 25 cmより近くは見えない、値段が高い |
| VL53L1X(レーザーToF) | 最大4 m | 小さくて軽く、ビームが鋭い | 日光の下では範囲が縮む、3.3 VのI2C |
| Sharp GP2Y0A02(赤外線) | 20〜150 cm | アナログ出力でコードが簡単 | 暗い面や光沢のある面にだまされる、消費電流は約33 mA |
距離に応じた振動とビープ音の間隔
視覚障害のある人は、周囲の状況の多くを音で感じ取っています。近づいてくる車、人混みのざわめき、杖でたたく音が壁に反響する音が、どれも道を見つける手がかりになります。ずっと鳴り続けるブザーは、助けになるどころかこうした音をかき消してしまいます。そのため、このプロジェクトのメインの通知手段は振動で、ルールは単純です。障害物が近いほど、振動のパルスは頻繁に、強くなります。150 cmより遠いものは無視し、障害物が30 cmより近ければモーターは連続して振動します。その間では、パルスの間隔をmap()で直線的に計算します。間隔 = 120 + (距離 − 30) × 6.5 msです。
なぜ150 cmなのでしょうか。白杖を使って歩く人は、1秒間に約1メートル進みます。150 cmの範囲があれば、障害物に着くまでに約1.5秒あります。触覚による通知への反応時間は0.2〜0.3秒なので、立ち止まったり向きを変えたりするには十分です。範囲を250 cmまで広げると、人通りの多い歩道では白杖が休みなく振動し、しばらくすると使う人が通知を気にしなくなってしまいます。屋内なら100 cm、屋外なら150〜200 cmがうまくいきます。変更が必要なのはFAR_CM定数だけです。
| 距離 | パルスの間隔 | 振動の強さ(PWM) | 使う人にとっての意味 |
|---|---|---|---|
| 150 cmより遠い | 振動なし | 0 | 道は空いている |
| 150 cm | 900 ms | 150 | 前方に障害物がある |
| 100 cm | 575 ms | 194 | 障害物が近づいている、注意 |
| 60 cm | 315 ms | 229 | 速度を落とし、向きを変える準備をする |
| 40 cm | 185 ms | 247 | すぐ近く |
| 30 cm以内 | 連続 | 255 | 止まる |

白杖でいちばんよく使われるのは、携帯電話に入っているようなコイン形のERMモーターです。約3 Vで動き、60〜80 mAほどの電流を流します。ところがArduinoのピンが供給できるのは最大40 mAで、安全な上限は20 mAなので、モーターはピンに直接つながず、トランジスタを通してつなぎます。完成品のモジュールには、このトランジスタと、モーターが止まるときの逆電圧を吸収するダイオードがすでに入っています。ほとんどのモジュールは3.3〜5 Vで動きます。モーター単体を使う場合は、BC337や2N2222のようなNPNトランジスタ、1 kΩのベース抵抗、モーターに逆向きに並列接続した1N4148ダイオードがあれば十分です。トランジスタはトランジスタツールで比べられます。
モーターをどこに付けるかは、コードと同じくらい重要です。持ち手の中の、手のひらか人差し指が当たる場所に置き、スポンジではなく薄いゴムで固定してください。スポンジは振動を吸収してしまうからです。コイン形モーターはフルスピードで約200 Hzで振動し、人の皮膚は200〜300 Hz付近の振動にいちばん敏感なので、正しい場所に置けば小さなモーターでもはっきり感じられます。ERMモーターはPWMの値がとても低いと回り始められないため、コードではいちばん弱い振動を255中150にしています。モーターの選び方やPWMでの強さの制御については、振動モーターの記事で詳しく説明しています。
配線図:Arduino Uno/Nano、ESP32、Raspberry Pi Pico
下の表は、この記事のすべてのコードと完全に対応しています。ピン番号はUnoとNanoで同じで、どちらのボードでもMPU6050のI2CバスはA4(SDA)とA5(SCL)です。振動モジュールのVCCは5Vへつなぎ、INピンはあえてD5にしています。tone()はTimer2を使うので、ブザーが鳴っている間はD3とD11のPWMが動かなくなりますが、D5はTimer0に属するので影響を受けません。Nanoのピン配置はピン配置ツールで確認できます。
| 接続 | Arduino Uno / Nano | ESP32 | Raspberry Pi Pico |
|---|---|---|---|
| HC-SR04 VCC | 5V | VIN(5 V) | VBUSまたは5 Vライン |
| HC-SR04 TRIG | D9 | GPIO 18 | GP2 |
| HC-SR04 ECHO | D10(直結) | GPIO 19、1 kΩ / 2 kΩの分圧回路経由 | GP3、1 kΩ / 2 kΩの分圧回路経由 |
| 振動モジュールIN | D5(PWM) | GPIO 25 | GP15 |
| パッシブブザー(+) | D8 | GPIO 26 | GP16 |
| ボタン(もう一方の脚はGNDへ) | D2 | GPIO 27 | GP14 |
| 水センサーの+ | D7 | GPIO 33 | GP22 |
| 水センサーのS | A1 | GPIO 34(ADC1) | GP26(ADC0) |
| MPU6050 SDA | A4 | GPIO 21 | GP4 |
| MPU6050 SCL | A5 | GPIO 22 | GP5 |
| MPU6050 VCC | 5V | 3V3 | 3V3 |
| 電池電圧の検出(100k/100k分圧回路の中点) | A0 | GPIO 35(ADC1) | GP28(ADC2) |
| NEO-6M GPS TX / RX | なし | GPIO 16 / GPIO 17 | なし |
| SIM800L TXD / RXD | なし | GPIO 13 / GPIO 23 | なし |
ステップごとの組み立て:まずは基本の白杖から
- まず回路をブレッドボード上で組み、白杖への取り付けは最後の最後まで残しておきます。Arduino Nanoをブレッドボードの中央に差し込み、5VとGNDを両側の電源ラインに引き出します。
- HC-SR04のVCCとGNDを電源ラインに、TRIGをD9に、ECHOをD10につなぎます。
- 振動モジュールのVCCを5Vに、GNDをGNDに、INピン(モジュールによってはS)をD5につなぎます。パッシブブザーのプラス側の脚をD8に、もう一方の脚をGNDに差し込みます。
- 下の基本コードを書き込み、9600 baudでシリアルモニタを開きます。手をゆっくりセンサーに近づけたり遠ざけたりしてください。画面の距離が変わり、それに合わせて振動が速くなったり遅くなったりするはずです。
- センサーを壁から150、100、60、30 cmの位置に構え、シリアルモニタの値をメジャーと比べます。差が1〜2 cmより大きければ、配線と電源を確認してください。
- すべて動いたら、水センサー、MPU6050、ボタンを追加して完全版のコードに進みます。
1// Temel akıllı baston: engel yaklaştıkça titreşim ve bip hızlanır / Basic smart cane: vibration and beeps speed up as an obstacle gets closer
2const int TRIG = 9, ECHO = 10; // HC-SR04
3const int VIB = 5; // titreşim modülü IN, PWM pini / vibration module IN, PWM pin
4const int BUZZER = 8; // pasif buzzer / passive buzzer
5
6const int NEAR_CM = 30; // bundan yakın: sürekli uyarı / closer than this: continuous alert
7const int FAR_CM = 150; // bundan uzak: sessiz / farther than this: silent
8const int PULSE_MS = 60; // bir titreşim darbesinin süresi / length of one vibration pulse
9
10unsigned long lastPing = 0, lastPulse = 0;
11int distanceCm = 999;
12int vibNow = -1;
13
14void setVib(int v) { // yalnızca değer değişince yaz / write only when the value changes
15 if (v != vibNow) { analogWrite(VIB, v); vibNow = v; }
16}
17
18int pingCm() {
19 digitalWrite(TRIG, LOW); delayMicroseconds(2);
20 digitalWrite(TRIG, HIGH); delayMicroseconds(10); // 10 µs tetik darbesi / 10 µs trigger pulse
21 digitalWrite(TRIG, LOW);
22 unsigned long t = pulseIn(ECHO, HIGH, 12000UL); // 12 ms ≈ 2 m, daha uzağı gerekmez / 12 ms ≈ 2 m, no need for more
23 return t == 0 ? 999 : (int)(t / 58); // 999 = yankı yok / 999 = no echo
24}
25
26void setup() {
27 Serial.begin(9600);
28 pinMode(TRIG, OUTPUT);
29 pinMode(ECHO, INPUT);
30 pinMode(VIB, OUTPUT);
31}
32
33void loop() {
34 unsigned long now = millis();
35
36 if (now - lastPing >= 60) { // ölçümler arası en az 60 ms / at least 60 ms between pings
37 lastPing = now;
38 distanceCm = pingCm();
39 Serial.println(distanceCm);
40 }
41
42 if (distanceCm < 2 || distanceCm > FAR_CM) { // yol açık / the path is clear
43 setVib(0);
44 } else if (distanceCm <= NEAR_CM) { // çok yakın: sürekli / very close: continuous
45 setVib(255);
46 if (now - lastPulse >= 120) { lastPulse = now; tone(BUZZER, 2500, 60); }
47 } else {
48 unsigned long period = map(distanceCm, NEAR_CM, FAR_CM, 120, 900); // yakın = sık / near = frequent
49 int strength = map(distanceCm, NEAR_CM, FAR_CM, 255, 150); // yakın = güçlü / near = strong
50 if (now - lastPulse >= period) {
51 lastPulse = now;
52 tone(BUZZER, 2000, 30); // kısa bip / short beep
53 }
54 setVib(now - lastPulse < PULSE_MS ? strength : 0);
55 }
56}コードのどこにも長いdelay()はありません。すべての処理はmillis()で記録したタイムスタンプで動くので、振動のパルスが測定を止めてしまうことはありません。pulseIn()のタイムアウトは12 msにしています。12,000 / 58 ≈ 200 cmなので、それより遠い反響はどのみち役に立たず、タイムアウトを短くすればループが止まるのを防げます。反響が戻らないと関数は999を返し、これは「道が空いている」とみなされます。setVib()は値が変わったときだけモーターに書き込みます。ESP32コアの2.xではanalogWrite()が呼ばれるたびにPWMユニットを設定し直すので、この小さな工夫のおかげで同じコードがESP32でもスムーズに動きます。
水たまりセンサー:白杖の先端が水に入ったら知らせる
水たまりは、白杖で気づくのが特に難しい障害物の一つです。先端が水に入っても、音も手ごたえも地面とほとんど変わりません。それなのに、数センチの水でも靴はびしょぬれになります。そこで、銅のパターンがくし形に組み合わさった安価な水位センサーを使います。パターンの一部は電源に、一部は信号線につながっていて、水がその間をつなぐと信号の電圧が上がり、analogRead()の値が大きくなります。乾いたセンサーは0に近い値を示し、先端が水に入ったセンサーはUnoでたいてい数百の値を返します。深く入るほど値は大きくなります。
センサーは白杖のいちばん下に、下端が先端から約1〜2 cm上に来るように、パターンを地面に対して垂直にして取り付けます。こうすれば乾いた地面では何にも触れず、深さ1〜2 cmを超える水たまりではパターンがぬれます。このセンサーのよく知られた弱点は、常に電源を入れておくと銅のパターンが電気分解ですぐに腐食してしまうことです。コードでの対策は、センサーの+ピンを5Vに直接つながずにD7につなぎ、測定の10 msのあいだだけ電源を入れることです。0.5秒に1回の読み取りなら、センサーに電気が流れているのは全体の2%の時間だけです。仕組みは水位センサーの記事で、雨滴センサーの基板との違いは雨センサーの記事で説明しています。

MPU6050による転倒検知
MPU6050は、3軸加速度センサーと3軸ジャイロスコープを1つのチップに収めたモーションセンサーで、I2Cで接続します。GY-521基板にはレギュレーターが載っているので5 Vで動かせます。初期アドレスは0x68です。このプロジェクトでは加速度センサーだけを使い、ライブラリを使わずに4つのレジスタを直接操作します。0x6Bに0を書き込むとチップがスリープから復帰し、0x1Aで内蔵の44 Hzフィルタをオンにし、0x1Cに0x10を書き込むと測定範囲が±8 gに広がります。そして0x3Bから始まる6バイトからX、Y、Zの加速度を読み取ります。初期設定の±2 gの範囲では、白杖が地面にぶつかった瞬間に簡単に飽和してしまい、衝撃の本当の大きさがわかりません。±8 gの範囲では、1 gが生の値で4096カウントに相当します。センサーについて詳しく知りたい場合は、MPU6050加速度センサーとジャイロスコープの記事を見てください。

コードは読み取りのたびに2つの量を計算します。1つ目は合成加速度で、g = √(ax² + ay² + az²)です。白杖をじっと持っているときや地面に倒れているときは約1 gで、強い衝撃があると数gまで跳ね上がります。2つ目は傾き、つまり白杖と垂直方向のなす角です。基板をXの矢印がシャフトに沿うように取り付けた場合、傾きはatan2(√(ay² + az²), |ax|)で、白杖を立てると0度、歩いているときは40〜55度、地面に倒れると85〜90度になります。人の転倒を検知するシステムは、まず短い自由落下(0 gに近い瞬間)を探します。しかし倒れていく白杖は先端を中心に回転する棒のように動くので、持ち手の近くのセンサーはその瞬間をとらえられないことがよくあります。そのため、このアルゴリズムは自由落下ではなく衝撃のあとに平らになって動かない状態を探します。
| 段階 | 条件 | 時間 | 何が起こる? |
|---|---|---|---|
| WALK | 合成加速度が2.5 gを超える | 無制限 | SETTLEに移る。障害物の通知は動き続ける |
| SETTLE | 揺れが収まるのを待つ | 1秒 | CHECKに移る |
| CHECK | 傾きが70°を超え、加速度が0.8〜1.2 gの範囲にとどまる | 2秒 | 条件が崩れたらWALKに戻り、保たれたらALARMへ |
| ALARM | 白杖を見つけられるよう、1秒ごとにビープ音と振動 | 20秒 | ボタンを押すか、白杖を拾い上げる(傾きが60°未満になる)と解除 |
| SOS | 音、LED、振動でモールス信号の... --- ... | 解除されるまで | 短押しで止まる。ESP32はSMSを送る |
この流れのおかげで、誤報のほとんどを防げます。歩きながら白杖で地面をたたいたときにも2.5 gを超える短い衝撃が出ることがありますが、1秒後には白杖はまだ手の中にあって40〜55度に傾いているので、確認の段階はすぐに取り消されます。使う人は気づきもしません。一方、白杖が倒れて平らなまま動かなければ、約3秒後にアラームが始まり、1秒ごとのビープ音を頼りに、白杖を落とした人が音で探せます。白杖を拾い上げれば、アラームは自動で止まります。20秒間誰も反応しなければ、コードは本当に転倒したのかもしれないと判断してSOSを始めます。また、ボタンを2秒間押し続ければ、いつでも手動でSOSを始められます。
テストとキャリブレーションのコード
しきい値は白杖ごと、路面ごと、センサーごとに少しずつ違います。メインのコードに進む前に、下のスケッチを書き込んでください。0.25秒ごとに、距離、水センサーの生の値、直近0.25秒間の最大加速度、傾きをシリアルモニタに表示します。値を表に書き留めておけば、プロジェクトレポートの結果のセクションもそのまま埋まります。
1// Test ve kalibrasyon: mesafe, su sensörü ham değeri, en yüksek ivme (g) ve eğim / Test and calibration: distance, raw water value, peak acceleration (g) and tilt
2#include <Wire.h>
3
4const int TRIG = 9, ECHO = 10;
5const int WATER_PWR = 7, WATER_SIG = A1;
6const byte MPU = 0x68;
7
8float peakG = 0, tilt = 0;
9unsigned long lastPrint = 0;
10
11void mpuWrite(byte reg, byte val) {
12 Wire.beginTransmission(MPU);
13 Wire.write(reg);
14 Wire.write(val);
15 Wire.endTransmission();
16}
17
18void readMpu() {
19 Wire.beginTransmission(MPU);
20 Wire.write(0x3B); // ACCEL_XOUT_H
21 if (Wire.endTransmission(false) != 0) return;
22 if (Wire.requestFrom(MPU, (uint8_t)6) != 6) return;
23 float a[3];
24 for (int i = 0; i < 3; i++) {
25 uint8_t hi = Wire.read();
26 uint8_t lo = Wire.read();
27 a[i] = (int16_t)(((uint16_t)hi << 8) | lo) / 4096.0; // ±8 g: 4096 LSB/g
28 }
29 float g = sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]);
30 if (g > peakG) peakG = g;
31 tilt = atan2(sqrt(a[1] * a[1] + a[2] * a[2]), fabs(a[0])) * 180.0 / PI; // X ekseni baston boyunca / X axis along the cane
32}
33
34int pingCm() {
35 digitalWrite(TRIG, LOW); delayMicroseconds(2);
36 digitalWrite(TRIG, HIGH); delayMicroseconds(10);
37 digitalWrite(TRIG, LOW);
38 unsigned long t = pulseIn(ECHO, HIGH, 25000UL);
39 return t == 0 ? 999 : (int)(t / 58);
40}
41
42void setup() {
43 Serial.begin(9600);
44 pinMode(TRIG, OUTPUT);
45 pinMode(ECHO, INPUT);
46 pinMode(WATER_PWR, OUTPUT);
47 Wire.begin();
48 mpuWrite(0x6B, 0x00); // uyku modundan çıkar / wake from sleep mode
49 mpuWrite(0x1A, 0x03); // 44 Hz alçak geçiren filtre / 44 Hz low-pass filter
50 mpuWrite(0x1C, 0x10); // ivme aralığı ±8 g / accelerometer range ±8 g
51}
52
53void loop() {
54 readMpu(); // olabildiğince sık oku, tepeyi yakala / read as often as possible to catch the peak
55 if (millis() - lastPrint >= 250) {
56 lastPrint = millis();
57 digitalWrite(WATER_PWR, HIGH); // sensörü yalnızca ölçerken besle / power the sensor only while measuring
58 delay(10);
59 int water = analogRead(WATER_SIG);
60 digitalWrite(WATER_PWR, LOW);
61 Serial.print(F("cm: ")); Serial.print(pingCm());
62 Serial.print(F(" su/water: ")); Serial.print(water);
63 Serial.print(F(" tepe/peak g: ")); Serial.print(peakG, 2);
64 Serial.print(F(" egim/tilt: ")); Serial.println(tilt, 0);
65 peakG = 0;
66 }
67}- 水センサーを、乾いた状態と、先端を水の入ったコップに1 cm浸した状態で測ります。たとえば乾いた状態で0〜20、ぬれた状態で520なら、WATER_LIMITにはその中間の270〜300を選びます。
- 白杖をいつもの持ち方で構え、歩くときのように左右に振ります。傾きを記録してください。たいてい40〜55度の範囲に収まり、PICKUP_DEG(60)より小さくなるはずです。
- 白杖を床に平らに置きます。傾きが85〜90、加速度が約0.95〜1.05 gになれば、LYING_DEG(70)と0.8〜1.2 gの範囲が正しいことを確認できます。
- 歩くときと同じように白杖で地面をたたき、gのピーク値を見ます。次に白杖をマットの上に10回落とします。IMPACT_Gは落としたときの値より低く設定してください。たたいたときの値がこのしきい値を超えても、傾きのチェックで除外されます。
- 壁から30、60、100、150 cmの位置でそれぞれ10回測り、平均をメジャーと比べます。
完全なコード:障害物、水、転倒、SOS、電池残量低下の警告
完全なコードでは4つの処理が互いを止めることなく同じループで動き、転倒検知はenumとswitch-caseで作った小さなステートマシンです。ボタンには3つの役割があります。歩いているときに短押しすると障害物のビープ音がオン・オフし(振動は常にオンのまま)、アラームやSOSの最中に短押しするとそれを解除し、2秒の長押しでいつでもSOSを始めます。高いビープ音は音オン、低いビープ音は音オフの合図です。
1// Akıllı baston, tam sürüm: engel, su birikintisi, düşme algılama, SOS ve pil uyarısı
2// Smart cane, full version: obstacles, puddles, fall detection, SOS and low-battery warning
3#include <Wire.h>
4
5// Pinler (Arduino Uno ve Nano) / Pins (Arduino Uno and Nano)
6const int BUTTON = 2; // buton, diğer bacağı GND / button, other leg to GND
7const int VIB = 5; // titreşim modülü IN (PWM) / vibration module IN (PWM)
8const int WATER_PWR = 7; // su sensörü + ucu / water sensor + pin
9const int BUZZER = 8; // pasif buzzer / passive buzzer
10const int TRIG = 9, ECHO = 10; // HC-SR04
11const int LED = 13; // dahili LED, SOS'ta yanıp söner / built-in LED, flashes during SOS
12const int BAT = A0; // 18650, 100k/100k bölücüyle / 18650 via a 100k/100k divider
13const int WATER_SIG = A1; // su sensörü S ucu / water sensor S pin
14const byte MPU = 0x68; // MPU6050, AD0 = GND
15
16// Ayarlar: test koduyla bulduğun değerleri yaz / Settings: use the values from the test sketch
17const int NEAR_CM = 30, FAR_CM = 150;
18const int PULSE_MS = 60;
19const int WATER_LIMIT = 300; // kuru ve ıslak okumaların ortası / midpoint of dry and wet readings
20const float IMPACT_G = 2.5; // düşme darbesi eşiği / fall impact threshold
21const float LYING_DEG = 70; // bundan büyük eğim = yerde yatıyor / tilt above this = lying flat
22const float PICKUP_DEG = 60; // bundan küçük eğim = kaldırıldı / tilt below this = picked up
23const unsigned long CANCEL_MS = 20000; // SOS'tan önce iptal süresi / time to cancel before SOS
24const float LOW_BAT_V = 3.4; // 18650 için düşük pil sınırı / low limit for an 18650 cell
25
26enum Mode { WALK, SETTLE, CHECK, ALARM, SOS };
27Mode mode = WALK;
28
29// SOS Mors kodu: artı = ses, eksi = sessizlik, birim 150 ms / SOS in Morse: plus = tone, minus = silence, unit 150 ms
30const int8_t SOS_PATTERN[] = {1, -1, 1, -1, 1, -3, 3, -1, 3, -1, 3, -3, 1, -1, 1, -1, 1, -7};
31const byte SOS_LEN = sizeof(SOS_PATTERN) / sizeof(SOS_PATTERN[0]);
32const unsigned long UNIT_MS = 150;
33
34bool soundOn = true, wet = false, waterAlert = false, waterHigh = false, sosOn = false;
35bool btnDown = false, longFired = false;
36int distanceCm = 999, vibNow = -1;
37int hist[3] = {999, 999, 999};
38byte histIdx = 0, sosStep = 0;
39float gTotal = 1.0, tilt = 45;
40unsigned long lastPing = 0, lastPulse = 0, lastMpu = 0, lastWater = 0, lastBat = 0, lastBeep = 0;
41unsigned long modeStart = 0, waterStart = 0, sosStepStart = 0, btnSince = 0;
42
43void setVib(int v) { // yalnızca değişince yaz / write only on change
44 if (v != vibNow) { analogWrite(VIB, v); vibNow = v; }
45}
46
47void mpuWrite(byte reg, byte val) {
48 Wire.beginTransmission(MPU);
49 Wire.write(reg);
50 Wire.write(val);
51 Wire.endTransmission();
52}
53
54bool readMpu() {
55 Wire.beginTransmission(MPU);
56 Wire.write(0x3B); // ACCEL_XOUT_H
57 if (Wire.endTransmission(false) != 0) return false;
58 if (Wire.requestFrom(MPU, (uint8_t)6) != 6) return false;
59 float a[3];
60 for (int i = 0; i < 3; i++) {
61 uint8_t hi = Wire.read();
62 uint8_t lo = Wire.read();
63 a[i] = (int16_t)(((uint16_t)hi << 8) | lo) / 4096.0; // ±8 g: 4096 LSB/g
64 }
65 gTotal = sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]);
66 tilt = atan2(sqrt(a[1] * a[1] + a[2] * a[2]), fabs(a[0])) * 180.0 / PI; // 0° dik, 90° yatay / 0° upright, 90° flat
67 return true;
68}
69
70int pingCm() {
71 digitalWrite(TRIG, LOW); delayMicroseconds(2);
72 digitalWrite(TRIG, HIGH); delayMicroseconds(10);
73 digitalWrite(TRIG, LOW);
74 unsigned long t = pulseIn(ECHO, HIGH, 12000UL); // 12 ms ≈ 2 m
75 return t == 0 ? 999 : (int)(t / 58);
76}
77
78int median3(int a, int b, int c) { // tek tük hatalı ölçümü eler / drops a single bad reading
79 if ((a <= b && b <= c) || (c <= b && b <= a)) return b;
80 if ((b <= a && a <= c) || (c <= a && a <= b)) return a;
81 return c;
82}
83
84// 0 = olay yok, 1 = kısa basış, 2 = uzun basış (2 s) / 0 = nothing, 1 = short press, 2 = long press (2 s)
85byte readButton(unsigned long now) {
86 bool pressed = digitalRead(BUTTON) == LOW;
87 if (pressed && !btnDown) { btnDown = true; longFired = false; btnSince = now; }
88 if (pressed && btnDown && !longFired && now - btnSince >= 2000) { longFired = true; return 2; }
89 if (!pressed && btnDown) {
90 btnDown = false;
91 if (!longFired && now - btnSince >= 30) return 1; // 30 ms'den kısası sıçramadır / shorter than 30 ms is bounce
92 }
93 return 0;
94}
95
96void setMode(Mode m, unsigned long now) {
97 mode = m;
98 modeStart = now;
99 sosStep = 0;
100 sosStepStart = now;
101 sosOn = false;
102 setVib(0);
103 noTone(BUZZER);
104 digitalWrite(LED, LOW);
105}
106
107void fallUpdate(unsigned long now) {
108 switch (mode) {
109 case WALK:
110 if (gTotal > IMPACT_G) setMode(SETTLE, now); // sert darbe / hard impact
111 break;
112 case SETTLE: // 1 s sarsıntının bitmesini bekle / wait 1 s for the shaking to stop
113 if (now - modeStart >= 1000) setMode(CHECK, now);
114 break;
115 case CHECK: // 2 s boyunca yatay ve hareketsiz mi? / flat and still for 2 s?
116 if (tilt < LYING_DEG || fabs(gTotal - 1.0) > 0.2) setMode(WALK, now); // yalnızca bir vuruştu / just a tap
117 else if (now - modeStart >= 2000) setMode(ALARM, now);
118 break;
119 case ALARM:
120 if (tilt < PICKUP_DEG) setMode(WALK, now); // baston kaldırıldı / the cane was picked up
121 else if (now - modeStart >= CANCEL_MS) setMode(SOS, now);
122 break;
123 default:
124 break;
125 }
126}
127
128void waterUpdate(unsigned long now) {
129 digitalWrite(WATER_PWR, HIGH); // yalnızca ölçerken besle: aşınmayı yavaşlatır / power only while measuring: slows corrosion
130 delay(10);
131 int v = analogRead(WATER_SIG);
132 digitalWrite(WATER_PWR, LOW);
133 if (v > WATER_LIMIT && !wet) { wet = true; waterAlert = true; waterStart = now; } // kurudan ıslağa / dry to wet
134 else if (v < WATER_LIMIT - 100) wet = false; // histerezis / hysteresis
135}
136
137void walkOutput(unsigned long now) {
138 // Ses kanalı: önce su uyarısı / Sound channel: water alert first
139 if (waterAlert && now - waterStart >= 1800) waterAlert = false;
140 if (waterAlert && now - lastBeep >= 300) {
141 lastBeep = now;
142 waterHigh = !waterHigh;
143 tone(BUZZER, waterHigh ? 1600 : 1000, 250); // iki tonlu uyarı / two-tone alert
144 }
145
146 // Titreşim kanalı: engel ne kadar yakınsa o kadar sık ve güçlü / Vibration channel: the closer, the faster and stronger
147 if (distanceCm < 2 || distanceCm > FAR_CM) {
148 setVib(0);
149 } else if (distanceCm <= NEAR_CM) {
150 setVib(255);
151 if (soundOn && !waterAlert && now - lastBeep >= 120) { lastBeep = now; tone(BUZZER, 2500, 60); }
152 } else {
153 unsigned long period = map(distanceCm, NEAR_CM, FAR_CM, 120, 900);
154 int strength = map(distanceCm, NEAR_CM, FAR_CM, 255, 150);
155 if (now - lastPulse >= period) {
156 lastPulse = now;
157 if (soundOn && !waterAlert) tone(BUZZER, 2000, 30);
158 }
159 setVib(now - lastPulse < PULSE_MS ? strength : 0);
160 }
161}
162
163void alarmOutput(unsigned long now) { // "bastonum düştü" sesi: bulmayı kolaylaştırır / "cane dropped" sound: helps finding it
164 setVib((now - modeStart) % 1000 < 150 ? 255 : 0);
165 if (now - lastBeep >= 1000) { lastBeep = now; tone(BUZZER, 2000, 150); }
166}
167
168void sosOutput(unsigned long now) {
169 if (now - sosStepStart >= (unsigned long)abs(SOS_PATTERN[sosStep]) * UNIT_MS) {
170 sosStep = (sosStep + 1) % SOS_LEN; // sonraki adım / next step
171 sosStepStart = now;
172 }
173 bool on = SOS_PATTERN[sosStep] > 0;
174 if (on != sosOn) {
175 sosOn = on;
176 if (on) tone(BUZZER, 3000); else noTone(BUZZER);
177 digitalWrite(LED, on);
178 setVib(on ? 255 : 0);
179 }
180}
181
182void batteryUpdate() {
183 float v = analogRead(BAT) * (5.0 / 1023.0) * 2.0; // bölücü gerilimi yarıya indirir / the divider halves the voltage
184 Serial.print(F("Pil / Battery: "));
185 Serial.println(v, 2);
186 if (v < LOW_BAT_V && mode == WALK) tone(BUZZER, 800, 400); // kalın, uzun bip / low, long beep
187}
188
189void setup() {
190 Serial.begin(9600);
191 pinMode(BUTTON, INPUT_PULLUP);
192 pinMode(TRIG, OUTPUT);
193 pinMode(ECHO, INPUT);
194 pinMode(VIB, OUTPUT);
195 pinMode(WATER_PWR, OUTPUT);
196 pinMode(LED, OUTPUT);
197 Wire.begin();
198 mpuWrite(0x6B, 0x00); // MPU6050'yi uyandır / wake the MPU6050
199 mpuWrite(0x1A, 0x03); // 44 Hz filtre / 44 Hz filter
200 mpuWrite(0x1C, 0x10); // ±8 g
201 tone(BUZZER, 1500, 100); // açılış sesi / power-on beep
202}
203
204void loop() {
205 unsigned long now = millis();
206
207 byte b = readButton(now);
208 if (b == 2) {
209 setMode(SOS, now); // uzun basış: elle SOS / long press: manual SOS
210 } else if (b == 1) {
211 if (mode == ALARM || mode == SOS) {
212 setMode(WALK, now); // alarmı ya da SOS'u iptal et / cancel the alarm or SOS
213 } else { // yürürken (SETTLE/CHECK dahil) sesi aç ya da kapat / while walking (incl. SETTLE/CHECK) sound on or off
214 soundOn = !soundOn;
215 tone(BUZZER, soundOn ? 2000 : 600, 120);
216 }
217 }
218
219 if (now - lastMpu >= 20) { // saniyede 50 ölçüm / 50 readings per second
220 lastMpu = now;
221 if (readMpu()) fallUpdate(now);
222 }
223
224 if (mode == WALK || mode == SETTLE || mode == CHECK) {
225 if (now - lastPing >= 60) {
226 lastPing = now;
227 hist[histIdx] = pingCm();
228 histIdx = (histIdx + 1) % 3;
229 distanceCm = median3(hist[0], hist[1], hist[2]);
230 }
231 if (now - lastWater >= 500) { lastWater = now; waterUpdate(now); }
232 walkOutput(now);
233 } else if (mode == ALARM) {
234 alarmOutput(now);
235 } else {
236 sosOutput(now);
237 }
238
239 if (now - lastBat >= 30000) { lastBat = now; batteryUpdate(); }
240}| 関数・設定 | 何をする? |
|---|---|
| pingCm() | 1回測定する。12 ms以内に反響が来なければ999を返す |
| median3() | 直近3回の測定値の中央値をとり、ときどき出るおかしな値を捨てる |
| readMpu() | 加速度データの6バイトを読み、合成加速度と傾きを計算する |
| fallUpdate() | WALK → SETTLE → CHECK → ALARM → SOSのステートマシンを動かす |
| waterUpdate() | 水センサーに10 msだけ電源を入れて読み、乾いた状態からぬれた状態に変わったら通知を始める |
| walkOutput() | 距離に応じた振動と、水の通知の2音の音を出す |
| sosOutput() | SOS_PATTERN配列を1ステップずつ再生する |
| readButton() | 短押し(1)と長押し(2)を区別し、30 msより短いチャタリングを無視する |
| IMPACT_G、LYING_DEG、CANCEL_MS | 転倒検知の感度と解除までの時間。テスト用スケッチで調整する |
SOSのパターンはモールス信号の決まりに従っています。短点は1単位、長点は3単位で、信号の間は1単位、文字の間は3単位、くり返しの間は7単位です。1単位を150 msにすると、SOSの1サイクルは34単位、約5.1秒になります。3 kHzの音はあえて選んだものです。人の耳は2〜4 kHzでいちばん敏感で、小さなパッシブブザーもこの範囲でいちばん大きな音が出ます。アクティブブザーとパッシブブザーの違いは、ブザーとは?の記事を参照してください。
電池、電源、動作時間
スマート白杖は屋外で一日中使うものなので、電源の設計はセンサーと同じくらい重要です。平均の消費電流を見積もってみましょう。Nanoは約20 mA、HC-SR04は15 mA、MPU6050は4 mAです。振動モーターは約70 mA流しますが、それはパルスの間だけで、ふつうの歩行では平均10 mAほどです。ブザーと水センサーで数mA加わります。合計でおよそ50 mA、5 Vで0.25 Wです。2600 mAhの18650セルは3.7 Vで約9.6 Whの電力量を持ち、効率85%の昇圧コンバーターを通すと約8 Whが回路に届きます。8 / 0.25 = 32時間です。電池を最後まで使い切れないことを考えると、現実的な数字は25〜30時間です。1日2時間使う人なら、2週間に1回ほど充電すればよいことになります。自分で計算するなら電池寿命計算ツールを使い、mAhとWhの違いはmAhとWhの記事を読んでください。
| 電源 | 重さ | 動作時間の目安(50 mA) | 評価 |
|---|---|---|---|
| 9 V電池(006P / PP3、約500 mAh)をVINへ | 45 g | 8〜10時間 | おすすめしない:モーターが動くと電圧が下がり、充電もできない |
| 単3形ニッケル水素電池4本(2000 mAh)を5Vピンへ | 約120 g | 35〜40時間 | 重い。満充電で5.6 Vになり、限界ぎりぎり |
| 18650×1本+TP4056+MT3608 | 約55 g | 25〜30時間 | おすすめ:軽く、USBで充電でき、保護回路付き |
| 18650×2本を直列(7.4 V)でVINへ | 約105 g | 45〜50時間 | 長持ちするが重く、別の充電回路が必要 |
| モバイルバッテリー(10,000 mAh) | 200 g以上 | 数日 | 重い。ほとんどのモバイルバッテリーは電流が小さいと自動で切れる |

おすすめの電源の構成は次のとおりです。18650セルをTP4056モジュールのB+とB−端子につなぎます。モジュールのOUT+からの線は、スライドスイッチを通ってMT3608昇圧モジュールの入力へ行きます。5.0 Vに設定したMT3608の出力で、Nanoの5Vピン、HC-SR04、振動モジュールに電源を供給します。電池電圧を測るため、スイッチのすぐ後ろに100 kΩの抵抗2本の分圧回路をつなぎ、中点をA0に入れます。スイッチの後ろにあるので、白杖の電源を切っている間は電池を消耗しません。DW01チップはセルを約2.4〜2.5 Vで切り離しますが、コードは3.4 Vを下回ると30秒ごとに低いビープ音を鳴らして、それよりずっと早く知らせます。詳しくはTP4056充電モジュールの記事を参照してください。
ケース、重さ、持ちやすさ
折りたたみ式の白杖の多くは200〜300 gで、使う人はそれを何時間も手首で左右に振ります。追加した1グラムごとに手からの距離が掛け合わされ、手首の負担になります。150 gのケースを持ち手の10 cm下に付けると、手首にかかる余分なトルクは150 × 10 = 1.5 kg·cmです。同じケースを60 cm下に付けると9 kg·cm、6倍になります。そのため電子回路はすべて1つのケースにまとめ、持ち手のすぐ下に置きます。先端まで下ろすのは、水センサーとその細いケーブルだけです。追加する重さは合計150 g未満を目指しましょう。
| 部品 | 重さの目安 |
|---|---|
| Arduino Nano | 7 g |
| HC-SR04 | 9 g |
| MPU6050(GY-521) | 2 g |
| 振動モジュール+パッシブブザー | 5 g |
| 水位センサーとケーブル | 8 g |
| 18650セル | 45〜48 g |
| TP4056+MT3608+スイッチ | 8 g |
| 3Dプリントのケースと固定具 | 40〜50 g |
| 配線、ネジ、面ファスナーのバンド | 10〜15 g |
| 合計 | 約135〜150 g |
- 素材:ケースはPETGかABSで印刷してください。PLAは日なたに止めた車の中でやわらかくなり、変形することがあります。壁の厚さは1.6〜2 mmで十分です。
- 取り付け:ケースはホースバンド2本か、内側にゴムを貼った面ファスナーのバンドで白杖に固定します。折りたたみ式の白杖では、ケースを最初の折りたたみ部分より上に付けないと、白杖が折りたためなくなります。
- センサーの窓:HC-SR04の2つの円筒はケースの前面から突き出るようにしてください。超音波は通り抜けられないので、前に薄いプラスチックや網を置かないでください。
- 雨:ケースの底に小さな水抜き穴をあけ、中にシリカゲルの小袋を入れます。雨の多い地域に住んでいるなら、HC-SR04の代わりに防水のJSN-SR04Tプローブを使いましょう。
- ボタン:クリック感のはっきりした大きなボタンを選び、持ち手の上で親指が自然に置かれる場所に付けます。うっかり押されないよう、まわりに高さ2 mmの縁を付けましょう。
- 電源スイッチ:スイッチの位置は手で触ってわかるようにします。電源を入れたときのビープ音でも、装置が動いていることを確かめられます。
- 充電口:TP4056のUSBソケットにはケースの底面から手が届くようにし、使わないときはゴム栓でふさぎます。
- 目立たせる:ケースの側面に反射テープを貼っておくと、夕方に道路を渡るとき、ドライバーが白杖に気づきやすくなります。
ESP32+GPSで位置情報を送る
Arduinoでは、SOSのときに白杖ができるのは近くにいる人に音で知らせることだけです。しかしESP32とGPSモジュールを使えば、白杖の位置をメッセージで家族に送れます。NEO-6Mモジュールは初期設定で9600 baudのNMEAセンテンスを出力し、1秒に1回測位します。TinyGPSPlusライブラリがこれを解析して、緯度、経度、衛星の数を教えてくれます。データシートによると、コールドスタートからの最初の測位には約27秒かかります。実際には屋外で30秒から数分かかり、屋内ではまったく測位できないこともよくあります。モジュールの小さなバックアップ電池が最後の衛星データを保持するので、2回目以降の起動では数秒で測位できます。詳しくはNEO-6M GPSモジュールの記事を参照してください。

メッセージを送る方法は2つあります。学校でのデモなら、いちばん簡単なのはESP32を使う人のスマホのテザリングにつなぎ、Telegramボットでメッセージを送る方法です。実際に街中で使う白杖なら、スマホに頼らないSIM800L GSMモジュールのほうが向いています。中に入れたSIMカードで直接SMSを送れるからです。SIM800Lには3.4〜4.4 Vが必要で、送信中は最大2 Aの電流が瞬間的に流れるので、昇圧コンバーターからではなく18650セルから直接電源を取り、電源ピンの間に1000 µFのコンデンサを入れてください。このモジュールは2Gネットワークでしか動きません。2Gが停波した国では、A7670やSIM7600のような4Gモジュールが必要です。日本ではGSM(2G)のサービスがそもそもないため、技適を取得したLTEモジュールを選んでください。すべてを1枚のボードにまとめたいなら、TTGO T-Call(ESP32+SIM800L)を使う方法もあります。
1// ESP32 akıllı baston: engel uyarısı, su ve düşme algılama, SOS'ta GPS konumunu SMS ile gönderme
2// ESP32 smart cane: obstacle alert, water and fall detection, sends the GPS location by SMS on SOS
3// Kütüphane: TinyGPSPlus (Mikal Hart) / Library: TinyGPSPlus (Mikal Hart)
4#include <Wire.h>
5#include <TinyGPSPlus.h>
6
7// Pinler / Pins
8const int TRIG = 18, ECHO = 19; // ECHO 1 kΩ / 2 kΩ bölücüyle / ECHO via a 1 kΩ / 2 kΩ divider
9const int VIB = 25, BUZZER = 26, BUTTON = 27;
10const int WATER_PWR = 33, WATER_SIG = 34; // ADC1 pini / ADC1 pin
11const int BAT = 35; // 100k/100k bölücü / 100k/100k divider
12const int GPS_RX = 16, GPS_TX = 17; // NEO-6M TX → 16, RX ← 17
13const int GSM_RX = 13, GSM_TX = 23; // SIM800L TXD → 13, RXD ← 23
14const byte MPU = 0x68; // SDA 21, SCL 22
15
16const char* PHONE = "+905XXXXXXXXX"; // SMS gidecek numara / number to text
17
18const int NEAR_CM = 30, FAR_CM = 150, PULSE_MS = 60;
19const int WATER_LIMIT = 1200; // 12 bit ölçekte, test ederek bul / on the 12-bit scale, find by testing
20const float IMPACT_G = 2.5, LYING_DEG = 70, PICKUP_DEG = 60;
21const unsigned long CANCEL_MS = 20000, SMS_REPEAT_MS = 300000; // 5 dakikada bir konum / location every 5 minutes
22
23enum Mode { WALK, SETTLE, CHECK, ALARM, SOS };
24Mode mode = WALK;
25const int8_t SOS_PATTERN[] = {1, -1, 1, -1, 1, -3, 3, -1, 3, -1, 3, -3, 1, -1, 1, -1, 1, -7};
26const int SOS_LEN = sizeof(SOS_PATTERN) / sizeof(SOS_PATTERN[0]);
27const unsigned long UNIT_MS = 150;
28
29TinyGPSPlus gps;
30bool soundOn = true, wet = false, waterAlert = false, waterHigh = false, sosOn = false, smsDue = false;
31bool btnDown = false, longFired = false;
32int distanceCm = 999, vibNow = -1, sosStep = 0, histIdx = 0;
33int hist[3] = {999, 999, 999};
34float gTotal = 1.0, tilt = 45;
35unsigned long lastPing = 0, lastPulse = 0, lastMpu = 0, lastWater = 0, lastBat = 0, lastBeep = 0;
36unsigned long modeStart = 0, waterStart = 0, sosStepStart = 0, btnSince = 0, lastSms = 0;
37
38void setVib(int v) {
39 if (v != vibNow) { analogWrite(VIB, v); vibNow = v; }
40}
41
42void mpuWrite(byte reg, byte val) {
43 Wire.beginTransmission(MPU);
44 Wire.write(reg);
45 Wire.write(val);
46 Wire.endTransmission();
47}
48
49bool readMpu() {
50 Wire.beginTransmission(MPU);
51 Wire.write(0x3B);
52 if (Wire.endTransmission(false) != 0) return false;
53 if (Wire.requestFrom(MPU, (uint8_t)6) != 6) return false;
54 float a[3];
55 for (int i = 0; i < 3; i++) {
56 uint8_t hi = Wire.read();
57 uint8_t lo = Wire.read();
58 a[i] = (int16_t)(((uint16_t)hi << 8) | lo) / 4096.0; // ±8 g
59 }
60 gTotal = sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]);
61 tilt = atan2(sqrt(a[1] * a[1] + a[2] * a[2]), fabs(a[0])) * 180.0 / PI;
62 return true;
63}
64
65int pingCm() {
66 digitalWrite(TRIG, LOW); delayMicroseconds(2);
67 digitalWrite(TRIG, HIGH); delayMicroseconds(10);
68 digitalWrite(TRIG, LOW);
69 unsigned long t = pulseIn(ECHO, HIGH, 12000UL);
70 return t == 0 ? 999 : (int)(t / 58);
71}
72
73int median3(int a, int b, int c) {
74 if ((a <= b && b <= c) || (c <= b && b <= a)) return b;
75 if ((b <= a && a <= c) || (c <= a && a <= b)) return a;
76 return c;
77}
78
79int readButton(unsigned long now) { // 0 yok, 1 kısa, 2 uzun / 0 none, 1 short, 2 long
80 bool pressed = digitalRead(BUTTON) == LOW;
81 if (pressed && !btnDown) { btnDown = true; longFired = false; btnSince = now; }
82 if (pressed && btnDown && !longFired && now - btnSince >= 2000) { longFired = true; return 2; }
83 if (!pressed && btnDown) {
84 btnDown = false;
85 if (!longFired && now - btnSince >= 30) return 1;
86 }
87 return 0;
88}
89
90void feedGps() { // NMEA cümlelerini çöz / parse the NMEA sentences
91 while (Serial2.available()) gps.encode(Serial2.read());
92}
93
94// SIM800L'den belirli bir yanıtı bekle / wait for a given reply from the SIM800L
95bool waitFor(const char* token, unsigned long timeoutMs) {
96 String buf;
97 unsigned long start = millis();
98 while (millis() - start < timeoutMs) {
99 while (Serial1.available()) {
100 buf += (char)Serial1.read();
101 if (buf.indexOf(token) >= 0) return true;
102 if (buf.length() > 200) buf.remove(0, 100);
103 }
104 feedGps(); // beklerken GPS'i kaçırma / keep reading GPS while waiting
105 }
106 return false;
107}
108
109bool sendSms(const String& text) {
110 Serial1.println("AT"); // ilk AT baud hızını eşler / the first AT syncs the baud rate
111 if (!waitFor("OK", 1000)) {
112 Serial1.println("AT");
113 if (!waitFor("OK", 1000)) return false;
114 }
115 Serial1.println("AT+CMGF=1"); // metin modu / text mode
116 if (!waitFor("OK", 1000)) return false;
117 Serial1.print("AT+CMGS=\"");
118 Serial1.print(PHONE);
119 Serial1.println("\"");
120 if (!waitFor(">", 5000)) return false; // mesaj istemi / message prompt
121 Serial1.print(text);
122 Serial1.write(26); // Ctrl+Z: gönder / Ctrl+Z: send
123 return waitFor("+CMGS", 20000); // şebeke onayı / network confirmation
124}
125
126String sosText() { // SMS'te Türkçe harf kullanma / no Turkish letters in the SMS
127 String s = "SOS! Akilli baston yardim cagrisi. ";
128 if (gps.location.isValid()) {
129 s += "Konum: https://maps.google.com/?q=";
130 s += String(gps.location.lat(), 6) + "," + String(gps.location.lng(), 6);
131 s += " (" + String(gps.location.age() / 1000) + " sn once, " + String(gps.satellites.value()) + " uydu)";
132 } else {
133 s += "GPS konumu henuz yok.";
134 }
135 return s;
136}
137
138void setMode(Mode m, unsigned long now) {
139 mode = m;
140 modeStart = now;
141 sosStep = 0;
142 sosStepStart = now;
143 sosOn = false;
144 setVib(0);
145 noTone(BUZZER);
146 if (m == SOS) { smsDue = true; lastSms = now; }
147}
148
149void fallUpdate(unsigned long now) {
150 if (mode == WALK && gTotal > IMPACT_G) setMode(SETTLE, now);
151 else if (mode == SETTLE && now - modeStart >= 1000) setMode(CHECK, now);
152 else if (mode == CHECK) {
153 if (tilt < LYING_DEG || fabs(gTotal - 1.0) > 0.2) setMode(WALK, now);
154 else if (now - modeStart >= 2000) setMode(ALARM, now);
155 } else if (mode == ALARM) {
156 if (tilt < PICKUP_DEG) setMode(WALK, now);
157 else if (now - modeStart >= CANCEL_MS) setMode(SOS, now);
158 }
159}
160
161void waterUpdate(unsigned long now) {
162 digitalWrite(WATER_PWR, HIGH);
163 delay(10);
164 int v = analogRead(WATER_SIG); // 0–4095
165 digitalWrite(WATER_PWR, LOW);
166 if (v > WATER_LIMIT && !wet) { wet = true; waterAlert = true; waterStart = now; }
167 else if (v < WATER_LIMIT - 400) wet = false;
168}
169
170void walkOutput(unsigned long now) {
171 if (waterAlert && now - waterStart >= 1800) waterAlert = false;
172 if (waterAlert && now - lastBeep >= 300) {
173 lastBeep = now;
174 waterHigh = !waterHigh;
175 tone(BUZZER, waterHigh ? 1600 : 1000, 250);
176 }
177 if (distanceCm < 2 || distanceCm > FAR_CM) {
178 setVib(0);
179 } else if (distanceCm <= NEAR_CM) {
180 setVib(255);
181 if (soundOn && !waterAlert && now - lastBeep >= 120) { lastBeep = now; tone(BUZZER, 2500, 60); }
182 } else {
183 unsigned long period = map(distanceCm, NEAR_CM, FAR_CM, 120, 900);
184 int strength = map(distanceCm, NEAR_CM, FAR_CM, 255, 150);
185 if (now - lastPulse >= period) {
186 lastPulse = now;
187 if (soundOn && !waterAlert) tone(BUZZER, 2000, 30);
188 }
189 setVib(now - lastPulse < PULSE_MS ? strength : 0);
190 }
191}
192
193void sosOutput(unsigned long now) {
194 if (smsDue || now - lastSms >= SMS_REPEAT_MS) { // girişte ve 5 dakikada bir / on entry and every 5 minutes
195 smsDue = false;
196 lastSms = now;
197 noTone(BUZZER);
198 bool ok = sendSms(sosText());
199 Serial.println(ok ? "SMS gonderildi / SMS sent" : "SMS gonderilemedi / SMS failed");
200 sosStepStart = millis();
201 sosOn = false;
202 }
203 if (now - sosStepStart >= (unsigned long)abs(SOS_PATTERN[sosStep]) * UNIT_MS) {
204 sosStep = (sosStep + 1) % SOS_LEN;
205 sosStepStart = now;
206 }
207 bool on = SOS_PATTERN[sosStep] > 0;
208 if (on != sosOn) {
209 sosOn = on;
210 if (on) tone(BUZZER, 3000); else noTone(BUZZER);
211 setVib(on ? 255 : 0);
212 }
213}
214
215void setup() {
216 Serial.begin(115200);
217 Serial2.begin(9600, SERIAL_8N1, GPS_RX, GPS_TX); // NEO-6M varsayılanı 9600 baud / NEO-6M default is 9600 baud
218 Serial1.begin(9600, SERIAL_8N1, GSM_RX, GSM_TX); // SIM800L
219 pinMode(BUTTON, INPUT_PULLUP);
220 pinMode(TRIG, OUTPUT);
221 pinMode(ECHO, INPUT);
222 pinMode(WATER_PWR, OUTPUT);
223 Wire.begin(21, 22);
224 mpuWrite(0x6B, 0x00);
225 mpuWrite(0x1A, 0x03);
226 mpuWrite(0x1C, 0x10);
227 tone(BUZZER, 1500, 100);
228}
229
230void loop() {
231 unsigned long now = millis();
232 feedGps();
233
234 int b = readButton(now);
235 if (b == 2) setMode(SOS, now);
236 else if (b == 1) {
237 if (mode == ALARM || mode == SOS) setMode(WALK, now); // alarmı ya da SOS'u iptal et / cancel the alarm or SOS
238 else { soundOn = !soundOn; tone(BUZZER, soundOn ? 2000 : 600, 120); } // yürürken: ses aç/kapat / while walking: sound on/off
239 }
240
241 if (now - lastMpu >= 20) {
242 lastMpu = now;
243 if (readMpu()) fallUpdate(now);
244 }
245
246 if (mode == WALK || mode == SETTLE || mode == CHECK) {
247 if (now - lastPing >= 60) {
248 lastPing = now;
249 hist[histIdx] = pingCm();
250 histIdx = (histIdx + 1) % 3;
251 distanceCm = median3(hist[0], hist[1], hist[2]);
252 }
253 if (now - lastWater >= 500) { lastWater = now; waterUpdate(now); }
254 walkOutput(now);
255 } else if (mode == ALARM) {
256 setVib((now - modeStart) % 1000 < 150 ? 255 : 0);
257 if (now - lastBeep >= 1000) { lastBeep = now; tone(BUZZER, 2000, 150); }
258 } else {
259 sosOutput(now);
260 }
261
262 if (now - lastBat >= 30000) {
263 lastBat = now;
264 float v = analogReadMilliVolts(BAT) * 2 / 1000.0; // kalibreli mV okuma / calibrated mV reading
265 Serial.printf("Pil / Battery: %.2f V, GPS: %s\n", v, gps.location.isValid() ? "var / fix" : "yok / no fix");
266 if (v < 3.4 && mode == WALK) tone(BUZZER, 800, 400);
267 }
268}コードはArduino版と同じステートマシンを使っていて、違いはSOSの部分にあります。SOSが始まるとすぐに、sendSms()がSIM800LにAT、テキストモードにするAT+CMGF=1、AT+CMGS="number"のコマンドを順に送り、「>」のプロンプトを待ってからメッセージを書き込み、Ctrl+Z(26)で送信します。位置情報はメッセージの中に地図のリンクとして入り、スマホでタップすると地図アプリが開きます。GPSがまだ測位できていなければメッセージにそう書かれ、SOSが続いている間は5分ごとに位置情報を送り直します。SMSの本文では、あえてトルコ語の文字を使っていません。ş、ğ、ıのような文字はGSMの標準アルファベットになく、一部のスマホでは文字化けするからです。日本語の文字も同じく標準アルファベットには含まれません。送信中、コードは最大30秒待つことがあります。また、SIM800Lは電源を入れてからネットワークに登録されるまでに10〜30秒かかります。
Raspberry Pi Picoで作るスマート白杖(MicroPython)
Raspberry Pi Picoでは、同じ考え方をMicroPythonで組み立てます。time_pulse_us()はArduinoのpulseIn()と同じように動き、反響が来なければ負の値を返します。PWM出力は16ビットのduty_u16()で設定し、振動モーターでは65535がフルパワーです。MPU6050の6バイトはi2c.readfrom_mem()で一度に読み、32767より大きい値から65536を引いて符号付きの数値に変換します。Picoのピンも3.3 Vなので、ここでもECHOの分圧回路は欠かせません。電池で動かすときは、MT3608の5 V出力をVSYSピンに入れます。HC-SR04も同じ5 Vラインから電源を取ります。Wi-Fi付きのPico Wなら、白杖の状態をWebページに表示することもできます。
1from machine import Pin, PWM, I2C, ADC, time_pulse_us
2import time, math
3
4# Pinler: ECHO 1 kΩ / 2 kΩ bölücüyle / Pins: ECHO via a 1 kΩ / 2 kΩ divider
5trig, echo = Pin(2, Pin.OUT), Pin(3, Pin.IN)
6i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400000) # MPU6050
7button = Pin(14, Pin.IN, Pin.PULL_UP)
8vib = PWM(Pin(15))
9vib.freq(1000)
10buzzer = PWM(Pin(16))
11water_pwr, water_sig = Pin(22, Pin.OUT), ADC(26)
12bat = ADC(28) # 100k/100k bölücü / 100k/100k divider
13
14NEAR_CM, FAR_CM, PULSE_MS = 30, 150, 60
15WATER_LIMIT = 20000 # 16 bit ölçekte, test ederek bul / on the 16-bit scale, find by testing
16IMPACT_G, LYING_DEG, PICKUP_DEG, CANCEL_MS = 2.5, 70, 60, 20000
17SOS = [1, -1, 1, -1, 1, -3, 3, -1, 3, -1, 3, -3, 1, -1, 1, -1, 1, -7] # Mors kodu / Morse code
18UNIT_MS = 150
19MPU = 0x68
20
21i2c.writeto_mem(MPU, 0x6B, b'\x00') # uyandır / wake up
22i2c.writeto_mem(MPU, 0x1A, b'\x03') # 44 Hz filtre / 44 Hz filter
23i2c.writeto_mem(MPU, 0x1C, b'\x10') # ±8 g
24
25beep_off = None
26
27def tone(freq, ms=0): # ms = 0: noTone'a kadar çalar / plays until quiet()
28 global beep_off
29 buzzer.freq(freq)
30 buzzer.duty_u16(32768) # %50 doluluk / 50% duty
31 beep_off = time.ticks_add(time.ticks_ms(), ms) if ms else None
32
33def quiet():
34 global beep_off
35 buzzer.duty_u16(0)
36 beep_off = None
37
38def ping_cm():
39 trig.low(); time.sleep_us(2)
40 trig.high(); time.sleep_us(10) # 10 µs tetik / 10 µs trigger
41 trig.low()
42 t = time_pulse_us(echo, 1, 12000) # yankı yoksa negatif / negative if no echo
43 return 999 if t < 0 else t // 58
44
45def read_mpu():
46 d = i2c.readfrom_mem(MPU, 0x3B, 6)
47 a = []
48 for i in (0, 2, 4):
49 v = (d[i] << 8) | d[i + 1]
50 if v > 32767:
51 v -= 65536 # işaretli 16 bit / signed 16-bit
52 a.append(v / 4096) # ±8 g: 4096 LSB/g
53 g = math.sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2])
54 tilt = math.atan2(math.sqrt(a[1] * a[1] + a[2] * a[2]), abs(a[0])) * 180 / math.pi
55 return g, tilt
56
57mode, mode_t = "walk", time.ticks_ms()
58sound_on, wet, water_alert, water_high = True, False, False, False
59hist, hist_i, dist = [999, 999, 999], 0, 999
60sos_i, sos_t, sos_on = 0, 0, None
61btn_down, long_fired, btn_t = False, False, 0
62last_ping = last_pulse = last_mpu = last_water = last_bat = last_beep = water_t = time.ticks_ms()
63
64def set_mode(m):
65 global mode, mode_t, sos_i, sos_t, sos_on
66 mode, mode_t = m, time.ticks_ms()
67 sos_i, sos_t, sos_on = 0, mode_t, None
68 vib.duty_u16(0)
69 quiet()
70
71def button_event(now): # 0 yok, 1 kısa, 2 uzun / 0 none, 1 short, 2 long
72 global btn_down, long_fired, btn_t
73 pressed = button.value() == 0
74 if pressed and not btn_down:
75 btn_down, long_fired, btn_t = True, False, now
76 if pressed and btn_down and not long_fired and time.ticks_diff(now, btn_t) >= 2000:
77 long_fired = True
78 return 2
79 if not pressed and btn_down:
80 btn_down = False
81 if not long_fired and time.ticks_diff(now, btn_t) >= 30:
82 return 1
83 return 0
84
85tone(1500, 100) # açılış sesi / power-on beep
86while True:
87 now = time.ticks_ms()
88 if beep_off is not None and time.ticks_diff(now, beep_off) >= 0:
89 quiet()
90
91 b = button_event(now)
92 if b == 2:
93 set_mode("sos") # uzun basış: elle SOS / long press: manual SOS
94 elif b == 1:
95 if mode in ("alarm", "sos"):
96 set_mode("walk") # alarmı ya da SOS'u iptal et / cancel the alarm or SOS
97 else: # yürürken: ses aç/kapat / while walking: sound on/off
98 sound_on = not sound_on
99 tone(2000 if sound_on else 600, 120)
100
101 if time.ticks_diff(now, last_mpu) >= 20: # 50 Hz
102 last_mpu = now
103 g, tilt = read_mpu()
104 el = time.ticks_diff(now, mode_t)
105 if mode == "walk" and g > IMPACT_G:
106 set_mode("settle")
107 elif mode == "settle" and el >= 1000:
108 set_mode("check")
109 elif mode == "check":
110 if tilt < LYING_DEG or abs(g - 1) > 0.2:
111 set_mode("walk") # yalnızca bir vuruş / just a tap
112 elif el >= 2000:
113 set_mode("alarm")
114 elif mode == "alarm":
115 if tilt < PICKUP_DEG:
116 set_mode("walk") # baston kaldırıldı / cane picked up
117 elif el >= CANCEL_MS:
118 set_mode("sos")
119
120 if mode in ("walk", "settle", "check"):
121 if time.ticks_diff(now, last_ping) >= 60:
122 last_ping = now
123 hist[hist_i] = ping_cm()
124 hist_i = (hist_i + 1) % 3
125 dist = sorted(hist)[1] # 3 ölçümün ortancası / median of 3
126 if time.ticks_diff(now, last_water) >= 500:
127 last_water = now
128 water_pwr.on()
129 time.sleep_ms(10)
130 w = water_sig.read_u16()
131 water_pwr.off()
132 if w > WATER_LIMIT and not wet:
133 wet, water_alert, water_t = True, True, now
134 elif w < WATER_LIMIT - 5000:
135 wet = False
136 if water_alert and time.ticks_diff(now, water_t) >= 1800:
137 water_alert = False
138 if water_alert and time.ticks_diff(now, last_beep) >= 300:
139 last_beep = now
140 water_high = not water_high
141 tone(1600 if water_high else 1000, 250) # iki tonlu su uyarısı / two-tone water alert
142 if dist < 2 or dist > FAR_CM:
143 vib.duty_u16(0)
144 elif dist <= NEAR_CM:
145 vib.duty_u16(65535)
146 if sound_on and not water_alert and time.ticks_diff(now, last_beep) >= 120:
147 last_beep = now
148 tone(2500, 60)
149 else:
150 period = 120 + (dist - NEAR_CM) * 780 // (FAR_CM - NEAR_CM) # 120–900 ms
151 strength = 65535 - (dist - NEAR_CM) * 27000 // (FAR_CM - NEAR_CM) # %100–%59
152 if time.ticks_diff(now, last_pulse) >= period:
153 last_pulse = now
154 if sound_on and not water_alert:
155 tone(2000, 30)
156 vib.duty_u16(strength if time.ticks_diff(now, last_pulse) < PULSE_MS else 0)
157 elif mode == "alarm":
158 vib.duty_u16(65535 if time.ticks_diff(now, mode_t) % 1000 < 150 else 0)
159 if time.ticks_diff(now, last_beep) >= 1000:
160 last_beep = now
161 tone(2000, 150) # bulma sesi / locator beep
162 else:
163 if time.ticks_diff(now, sos_t) >= abs(SOS[sos_i]) * UNIT_MS:
164 sos_i = (sos_i + 1) % len(SOS)
165 sos_t = now
166 on = SOS[sos_i] > 0
167 if on != sos_on:
168 sos_on = on
169 if on:
170 tone(3000)
171 else:
172 quiet()
173 vib.duty_u16(65535 if on else 0)
174
175 if time.ticks_diff(now, last_bat) >= 30000:
176 last_bat = now
177 v = bat.read_u16() * 3.3 / 65535 * 2
178 print("Pil / Battery:", round(v, 2), "V")
179 if v < 3.4 and mode == "walk":
180 tone(800, 400)
181
182 time.sleep_ms(2)科学コンテストや競技会で発表するときのコツ
スマート白杖は科学コンテストや学校の競技会でおなじみのテーマです(ほかのアイデアはArduinoの科学コンテスト向けプロジェクトアイデアの記事にあります)。そのため審査員の心をつかむのは、白杖が振動することではなく、問題をどれだけうまく定義し、解決策をどれだけ誠実に測定したかです。いちばん説得力のあるプロジェクトは、視覚障害のある人や支援団体との対話から始まります。どの障害物が本当に困るのか、振動と音のどちらが好きか、白杖はどのくらいの重さまでなら許せるかを本人から聞くことで、プロジェクトに方向性と力強いストーリーの両方が生まれます。
- 問題:白杖は腰より上の障害物を見つけられず、水たまりにも気づけません。インタビューで聞いた具体例を使って説明しましょう。
- 目的:白杖の重さや使い方をあまり変えずに、体と頭の高さの障害物を少なくとも1秒前に振動で知らせる追加装置を設計すること。
- 方法:センサーの選択(HC-SR04、JSN-SR04T、ToF)とその理由、取り付け角度とその計算、振動の対応づけとその式を示します。
- 結果:下のテストの結果を表とグラフで示し、うまくいかなかったケースも含めます。
- 結論と提言:プロジェクトが何を解決するのか、その限界(ガラス、柔らかい面、雨、使う人ではなく白杖の転倒を検知すること)、そして次のステップをはっきり述べます。
| テスト | やり方 | 記録する値 |
|---|---|---|
| 距離の精度 | 壁から30、60、100、150 cmでそれぞれ10回測定 | 平均誤差(cm)と最大のずれ |
| 障害物の種類 | 壁、人、直径5 cmのポール、茂み、ガラスのドア、布のカーテン | 各障害物を10回中何回検出したか |
| 反応時間 | スマホのスロー動画で、障害物が現れてから最初の振動まで | 遅れ(ミリ秒) |
| 水の検出 | 先端を0.5、1、2、3 cmの水に浸す | どの深さから警告するか |
| 転倒検知 | 20回落とし、地面を100回たたく | 正しいアラームと誤報の回数 |
| 電池の持ち | 満充電から電源が切れるまで動かす | 時間(時間単位)と最終電圧 |
| 重さとバランス | キッチンスケールで量り、持ち手からの距離を測る | 追加した重さ(g)とトルク(kg·cm) |
改良のアイデア
- 段差と穴の警告:前方の下に向けた2つ目のHC-SR04は、ふだんは一定の距離に地面を見ています。測定値が急に長くなったら、前方に下り段差か穴があります。
- 音声メッセージ:DFPlayer Miniを使って、ビープ音の代わりに「前方に障害物」「水」「電池残量低下」といった録音した言葉をイヤホンで再生します。
- スマホアプリ:ESP32のBluetooth BLEを使って、電池残量と設定をスマホに表示し、そこから範囲を変更します。
- 方向:持ち手の左右に別々のモーターを付け、障害物がどちら側にあるかを振動で示します。
- スリープモード:白杖が何分も動いていないことをMPU6050が検知したら、センサーの電源を切ります。ESP32のディープスリープを使えば、電池の持ちが何倍にもなります。
- 夜間の視認性:LDRを使って、暗くなったら白杖の先端のLEDを点滅させ、ドライバーが遠くから使う人に気づけるようにします。
よくある失敗と直し方
| 症状 | 考えられる原因 | 対処 |
|---|---|---|
| 白杖がずっと振動し続ける | センサーが地面か白杖そのものを向いている | 取り付け角度を上げ、センサーの前に何もないようにする |
| ときどき理由もなく振動する | たまに出るおかしな反響か、近くにある別の超音波源 | メディアンフィルタはすでに入っている。必要なら5回の測定値の中央値をとる |
| 遠い距離でモーターが回らない | PWMの値がモーターの起動しきい値より低い | 最小の強さを150より上に保ち、モーターをゴムで固定する |
| モーターが動くとArduinoが再起動する | モーターの電流で電源電圧が下がっている、またはダイオードがない | 完成品のモジュールを使い、電源に100 µFのコンデンサを追加する |
| MPU6050がいつも0か一定の値を返す | SDAとSCLが逆、アドレスが0x69、またはチップがスリープしている | I2Cスキャナーでアドレスを調べ、レジスタ0x6Bに0が書き込まれているか確認する |
| 地面をたたくと誤報が出る | IMPACT_Gが低く、傾きのしきい値が歩くときの角度に近い | テスト用スケッチで傾きを測り、LYING_DEGを75に上げる |
| 水の通知が止まらない、またはまったく鳴らない | しきい値が間違っているか、パターンが酸化している | 乾いた状態とぬれた状態の値を測り直し、必要ならセンサーを交換する |
| ブザーが鳴らない、またはいつも同じ音しか出ない | アクティブブザーが付いている | tone()にはパッシブブザーを使う |
| SMSが送られない | SIM800Lがネットワークに登録されていない、電源が弱い、またはSIMのPINが有効になっている | モジュールのLEDが3秒ごとに点滅しているか確認し、PINを無効にして、モジュールの電源を18650から取る |
スマート白杖は、距離の測定、触覚によるフィードバック、モーションセンサーによるイベント検知、電源の設計を1つのプロジェクトにまとめたもので、現実の困りごとに応えるため、Arduinoで作れるプロジェクトの中でも特に意義のあるものの一つです。まずこのガイドの基本コードから始め、テストの表を埋めてから、水、転倒、GPSの機能を1つずつ加えていきましょう。BlueGraysアプリには、HC-SR04による距離の測定、パーキングセンサー、ブザーで音を鳴らすプロジェクト、ESP32の通知プロジェクトが、回路図とすぐに使えるコード付きでステップごとにそろっています。白杖の各部分を、まずは単体で試せます。
よくある質問
スマート白杖はどのように動くのですか?
持ち手の近くに取り付けたHC-SR04超音波センサーが音波を送り出し、反響が戻るまでの時間から前方の障害物までの距離を計算します。その距離をもとに、Arduinoが持ち手の振動モーターを動かします。障害物が近いほど、振動は速く強くなります。より高度な版では、先端の水センサーが水たまりを検知し、MPU6050が白杖の転倒を検知します。
Arduinoのスマート白杖にはどんな部品が必要ですか?
基本の版なら、Arduino Nano、HC-SR04超音波センサー、振動モーターモジュール、パッシブブザー、ジャンパーワイヤーがあれば十分です。完全版では、水位センサー、MPU6050、ボタン、18650セルとTP4056充電モジュール、MT3608昇圧モジュールが加わります。位置情報を送るには、ESP32、NEO-6M GPS、SIM800L GSMモジュールが必要です。
スマート白杖はどのくらい先の障害物を検知できますか?
HC-SR04は2〜400 cmを測れますが、白杖ではふつう150 cmを上限にします。秒速約1メートルで歩く人にとって、これは障害物に着く1.5秒前の警告になります。範囲をずっと長くすると、人の多い場所では振動が止まらなくなり、使う人が通知を気にしなくなってしまいます。
スマート白杖は、倒れたことをどうやって知るのですか?
MPU6050加速度センサーを1秒間に50回読み取ります。2.5 gを超える衝撃のあと、白杖が2秒間平らなまま動かなければ倒れたとみなし、居場所を知らせるビープ音を鳴らし始めます。20秒以内に白杖が拾い上げられず、ボタンも押されなければSOSが鳴ります。ESP32版では、その時点で位置情報をSMSで送ります。
Arduinoのスマート白杖は電池でどのくらい動きますか?
回路の消費電流は平均で約50 mAです。TP4056充電モジュールとMT3608昇圧モジュールを組み合わせた2600 mAhの18650セル1本で約25〜30時間動き、1日2時間使うとしておよそ2週間もちます。9 V電池(006P / PP3)は8〜10時間しかもたず、モーターが動くと電圧が下がるのでおすすめしません。
スマート白杖にGPSの位置情報共有を追加できますか?
はい。ArduinoのかわりにESP32を使い、NEO-6M GPSモジュールを追加すれば、白杖の緯度と経度を読み取れます。SIM800L GSMモジュールを使えば、SOSのときに位置情報が地図のリンクとしてSMSで家族に届きます(日本では技適を取得したLTEモジュールを使います)。Wi-Fiがあれば、Telegramボットも使えます。GPSは屋外では動きますが、屋内ではたいてい測位できません。
この記事を読んだ後に作れるプロジェクト
全プロジェクトを見る
HC-SR04で距離を測定(シリアルモニタ表示)
HC-SR04センサーは、コウモリが暗闇で道を見つけるのと同じ仕組みで動作します。人には聞こえない音を出し、その反響を捉えて前方の物体までの距離を測ります。Arduinoは0.5秒ごとに距離をセンチメートル単位でコンピューターの画面に表示し、3〜100センチメートルの測定値を示します。駐車センサーや障害物を避けるロボットなどのプロジェクトの基礎になります。

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

ブザーの周波数を調整
4つのボタンでブザーの音を操作します。1つは音を鳴らし、1つは音を止め、残りの2つは音を高くしたり低くしたりします。音を高くするボタンや低くするボタンを押し続けると、音程が段階的に変化し、サイレンのように聞こえます。音の高低が生まれる理由を、自分の耳で確かめられます。

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駐車センサー(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件以上用意しています。

