ガイド · 読了まで27分

Arduinoで作るスマート白杖:障害物・水たまり・転倒を知らせる

Arduinoでスマート白杖を作る方法。HC-SR04による障害物検知、振動での通知、水センサー、MPU6050による転倒検知、SOS、完全なコードを解説します。

Arduinoのスマート白杖は、白杖では見つけられない障害物を視覚障害のある人に知らせる装置で、学校の課題や科学コンテストで特に人気のある電子工作プロジェクトの一つです。持ち手の近くに取り付けたHC-SR04超音波センサーが前方にあるものまでの距離を測ります。障害物が近づくほど持ち手の振動モーターが速く強く震え、必要ならブザーも振動のたびに短いビープ音を鳴らします。これで、杖の先端が決して触れない腰より上の障害物を、数歩手前で感じ取れるようになります。

このガイドでは基本の回路から始め、プロジェクトを本当に使えるものにする追加機能をステップごとに加えていきます。白杖の先端近くの水たまりセンサー、MPU6050で白杖が倒れたことに気づき、20秒以内に誰もキャンセルしなければSOSを鳴らすアルゴリズム、18650セル1本で25時間以上動く電源回路、重さと持ちやすさを考えたケース、そして位置情報をSMSで送るESP32+GPSの拡張です。最後には、プロジェクトのテスト方法と科学コンテストでの発表方法についての具体的なコツも紹介します。

スマート白杖用のHC-SR04超音波距離センサー
HC-SR04は40 kHzの音のパルスを送り、反響が戻るまでの時間から距離を計算します。スマート白杖では、約15度の細い円すいの中で2〜150 cmの障害物を見張ります。

スマート白杖とは?何をするもの?

白杖は、視覚障害のある人がいちばん頼りにしている道具です。杖でたたくたびに、路面、縁石、段差、前方の低い障害物がわかります。しかし白杖が「見る」ことができるのは、先端が触れたものだけです。低く張り出した木の枝、歩道の上に突き出た看板、トラックのサイドミラー、開いた窓、壁から飛び出したエアコンの室外機など、腰より上の障害物は先端に触れることがなく、代わりに体や頭にぶつかります。スマート白杖は、このすき間を埋めるために設計されています。電子センサーが白杖の届かない高さと距離を調べ、その結果を振動として手に伝えます。

最初に一つはっきりさせておきましょう。スマート白杖は白杖や歩行訓練の代わりになるものではなく、それを補うものです。超音波センサーには死角があり、電池は切れます。そのため設計の鉄則は、電子回路が完全にオフになっても白杖としては以前とまったく同じように使えることです。毎年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プリントか小さな工作用ケース
Arduino Nano
Arduino Nano
パッシブブザー
パッシブブザー
押しボタン
押しボタン
18650リチウムイオン電池
18650リチウムイオン電池

障害物の検知: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-SR042〜400 cm安い、簡単、円すいが広い防水ではない、柔らかい面が苦手
JSN-SR04T約25〜450 cm防水プローブで雨の中でも使える25 cmより近くは見えない、値段が高い
VL53L1X(レーザーToF)最大4 m小さくて軽く、ビームが鋭い日光の下では範囲が縮む、3.3 VのI2C
Sharp GP2Y0A02(赤外線)20〜150 cmアナログ出力でコードが簡単暗い面や光沢のある面にだまされる、消費電流は約33 mA
超音波センサーの死角を知っておきましょう。厚いコート、茂み、カーテンのような柔らかい面は音を吸収し、ガラスのドアや傾いたパネルは音をよそへ反射するので、障害物がまったく検出されないことがあります。直径3〜4 cmの細いポールも、円すいの中では小さいため気づくのが遅れます。だからこそ、スマート白杖を唯一の安全手段として紹介してはいけません。これは白杖と一緒に働くアシスタントです。

距離に応じた振動とビープ音の間隔

視覚障害のある人は、周囲の状況の多くを音で感じ取っています。近づいてくる車、人混みのざわめき、杖でたたく音が壁に反響する音が、どれも道を見つける手がかりになります。ずっと鳴り続けるブザーは、助けになるどころかこうした音をかき消してしまいます。そのため、このプロジェクトのメインの通知手段は振動で、ルールは単純です。障害物が近いほど、振動のパルスは頻繁に、強くなります。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 cm900 ms150前方に障害物がある
100 cm575 ms194障害物が近づいている、注意
60 cm315 ms229速度を落とし、向きを変える準備をする
40 cm185 ms247すぐ近く
30 cm以内連続255止まる
Arduino用の振動モーターモジュール
振動モーターモジュール:直径約10 mmのコイン形モーターの横に、それを駆動するトランジスタとフライバックダイオードが載っています。INピンはArduinoのPWMピンに直接つなぎ、モーターの電流はモジュール側が受け持ちます。

白杖でいちばんよく使われるのは、携帯電話に入っているようなコイン形の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 / NanoESP32Raspberry Pi Pico
HC-SR04 VCC5VVIN(5 V)VBUSまたは5 Vライン
HC-SR04 TRIGD9GPIO 18GP2
HC-SR04 ECHOD10(直結)GPIO 19、1 kΩ / 2 kΩの分圧回路経由GP3、1 kΩ / 2 kΩの分圧回路経由
振動モジュールIND5(PWM)GPIO 25GP15
パッシブブザー(+)D8GPIO 26GP16
ボタン(もう一方の脚はGNDへ)D2GPIO 27GP14
水センサーの+D7GPIO 33GP22
水センサーのSA1GPIO 34(ADC1)GP26(ADC0)
MPU6050 SDAA4GPIO 21GP4
MPU6050 SCLA5GPIO 22GP5
MPU6050 VCC5V3V33V3
電池電圧の検出(100k/100k分圧回路の中点)A0GPIO 35(ADC1)GP28(ADC2)
NEO-6M GPS TX / RXなしGPIO 16 / GPIO 17なし
SIM800L TXD / RXDなしGPIO 13 / GPIO 23なし
HC-SR04のECHOピンは5 Vのパルスを出しますが、ESP32とPicoのピンは3.3 Vで動き、5 Vトレラントではありません。ECHO → 1 kΩ → ボードへの線 → 2 kΩ → GNDという分圧回路を組めば、ピンには約3.3 Vが届きます。ESP32では、Wi-FiがオンのあいだはADC2が動かないので、アナログの読み取りにはADC1のピン(GPIO 32〜39)を使ってください。WROVERモジュールを載せたESP32ボードでは、GPIO 16と17がPSRAMにつながっています。そうしたボードなら、GPSを別の2本のピンに移してください。すべてのモジュールのGNDを共通にする必要があります。

ステップごとの組み立て:まずは基本の白杖から

  1. まず回路をブレッドボード上で組み、白杖への取り付けは最後の最後まで残しておきます。Arduino Nanoをブレッドボードの中央に差し込み、5VとGNDを両側の電源ラインに引き出します。
  2. HC-SR04のVCCとGNDを電源ラインに、TRIGをD9に、ECHOをD10につなぎます。
  3. 振動モジュールのVCCを5Vに、GNDをGNDに、INピン(モジュールによってはS)をD5につなぎます。パッシブブザーのプラス側の脚をD8に、もう一方の脚をGNDに差し込みます。
  4. 下の基本コードを書き込み、9600 baudでシリアルモニタを開きます。手をゆっくりセンサーに近づけたり遠ざけたりしてください。画面の距離が変わり、それに合わせて振動が速くなったり遅くなったりするはずです。
  5. センサーを壁から150、100、60、30 cmの位置に構え、シリアルモニタの値をメジャーと比べます。差が1〜2 cmより大きければ、配線と電源を確認してください。
  6. すべて動いたら、水センサー、MPU6050、ボタンを追加して完全版のコードに進みます。
C++ (Arduino IDE)
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%の時間だけです。仕組みは水位センサーの記事で、雨滴センサーの基板との違いは雨センサーの記事で説明しています。

Arduino用の水位センサー
水位センサー:くし形に組み合わさった銅のパターンを水がつなぐと、Sピンの電圧が上がります。スマート白杖では先端から1〜2 cm上に取り付け、測定の瞬間だけ電源を入れます。
センサーのパターンがない上部とコネクターは、ホットボンドかマニキュアで覆ってください。水が触れてよいのは検出用のパターンだけです。ケーブルを白杖の外にぶら下げたままにしないでください。アルミ製の白杖なら中を通すか、熱収縮チューブに入れてシャフトに巻き付けます。より丈夫にしたいなら、先端のすぐ上に1 cm離してステンレスのネジを2本取り付けます。片方はD7へ、もう片方はA1へつなぎ、A1は100 kΩの抵抗でGNDにプルダウンします。水が2本のネジの間をつなぐとA1の値が上がります。コードはそのままで、WATER_LIMITを測り直すだけです。

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加速度センサーとジャイロスコープの記事を見てください。

MPU6050加速度・ジャイロセンサーモジュール(GY-521)
MPU6050(GY-521):3軸の加速度センサーとジャイロスコープです。基板に印刷されたXの矢印が白杖のシャフトに沿うように、ケースの中に固定します。

コードは読み取りのたびに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を始められます。

この方法が検知するのは白杖が倒れたことで、使う人が倒れたことではありません。倒れるときに白杖を握ったままだったり、白杖が壁に立てかかった状態で止まったりすると、アラームは作動しません。そのため手動のSOSボタンは必ず残し、発表ではこの限界をはっきり説明してください。体の転倒を直接測るには、ベルトや手首に着ける別の装置にセンサーを入れる必要があります。

テストとキャリブレーションのコード

しきい値は白杖ごと、路面ごと、センサーごとに少しずつ違います。メインのコードに進む前に、下のスケッチを書き込んでください。0.25秒ごとに、距離、水センサーの生の値、直近0.25秒間の最大加速度、傾きをシリアルモニタに表示します。値を表に書き留めておけば、プロジェクトレポートの結果のセクションもそのまま埋まります。

C++ (Arduino IDE)
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. 水センサーを、乾いた状態と、先端を水の入ったコップに1 cm浸した状態で測ります。たとえば乾いた状態で0〜20、ぬれた状態で520なら、WATER_LIMITにはその中間の270〜300を選びます。
  2. 白杖をいつもの持ち方で構え、歩くときのように左右に振ります。傾きを記録してください。たいてい40〜55度の範囲に収まり、PICKUP_DEG(60)より小さくなるはずです。
  3. 白杖を床に平らに置きます。傾きが85〜90、加速度が約0.95〜1.05 gになれば、LYING_DEG(70)と0.8〜1.2 gの範囲が正しいことを確認できます。
  4. 歩くときと同じように白杖で地面をたたき、gのピーク値を見ます。次に白杖をマットの上に10回落とします。IMPACT_Gは落としたときの値より低く設定してください。たたいたときの値がこのしきい値を超えても、傾きのチェックで除外されます。
  5. 壁から30、60、100、150 cmの位置でそれぞれ10回測り、平均をメジャーと比べます。

完全なコード:障害物、水、転倒、SOS、電池残量低下の警告

完全なコードでは4つの処理が互いを止めることなく同じループで動き、転倒検知はenumとswitch-caseで作った小さなステートマシンです。ボタンには3つの役割があります。歩いているときに短押しすると障害物のビープ音がオン・オフし(振動は常にオンのまま)、アラームやSOSの最中に短押しするとそれを解除し、2秒の長押しでいつでもSOSを始めます。高いビープ音は音オン、低いビープ音は音オフの合図です。

C++ (Arduino IDE)
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 g8〜10時間おすすめしない:モーターが動くと電圧が下がり、充電もできない
単3形ニッケル水素電池4本(2000 mAh)を5Vピンへ約120 g35〜40時間重い。満充電で5.6 Vになり、限界ぎりぎり
18650×1本+TP4056+MT3608約55 g25〜30時間おすすめ:軽く、USBで充電でき、保護回路付き
18650×2本を直列(7.4 V)でVINへ約105 g45〜50時間長持ちするが重く、別の充電回路が必要
モバイルバッテリー(10,000 mAh)200 g以上数日重い。ほとんどのモバイルバッテリーは電流が小さいと自動で切れる
TP4056リチウムイオン充電モジュール
TP4056充電モジュール:USBから18650セルを1 Aで4.2 Vまで充電します。DW01保護チップ付きの版は、過放電やショートからもセルを守ります。負荷はOUT+とOUT−につなぎます。

おすすめの電源の構成は次のとおりです。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充電モジュールの記事を参照してください。

MT3608の出力は半固定抵抗(トリマー)で最大28 Vまで上げられ、工場出荷時にどんな値に設定されているかわかりません。Nanoにつなぐ前にテスターで出力を測り、5.0 Vになるまでトリマーを回してください。設定を誤った昇圧コンバーターは、数秒でボードを壊してしまいます。テスト中にUSBだけでボードに電源を入れている場合、A0はどこにもつながっていない状態になり、電池の値はでたらめになって、電池残量低下のビープ音が鳴ることがあります。これは正常です。

ケース、重さ、持ちやすさ

折りたたみ式の白杖の多くは200〜300 gで、使う人はそれを何時間も手首で左右に振ります。追加した1グラムごとに手からの距離が掛け合わされ、手首の負担になります。150 gのケースを持ち手の10 cm下に付けると、手首にかかる余分なトルクは150 × 10 = 1.5 kg·cmです。同じケースを60 cm下に付けると9 kg·cm、6倍になります。そのため電子回路はすべて1つのケースにまとめ、持ち手のすぐ下に置きます。先端まで下ろすのは、水センサーとその細いケーブルだけです。追加する重さは合計150 g未満を目指しましょう。

部品重さの目安
Arduino Nano7 g
HC-SR049 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モジュールの記事を参照してください。

セラミックアンテナ付きのNEO-6M GPSモジュール
NEO-6M GPSモジュール:セラミックアンテナは空が見える必要があるので、ケースの上面の、金属部品から離れた場所に置きます。初期設定のボーレートは9600です。

メッセージを送る方法は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)を使う方法もあります。

C++ (Arduino IDE)
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ページに表示することもできます。

MicroPython
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. 問題:白杖は腰より上の障害物を見つけられず、水たまりにも気づけません。インタビューで聞いた具体例を使って説明しましょう。
  2. 目的:白杖の重さや使い方をあまり変えずに、体と頭の高さの障害物を少なくとも1秒前に振動で知らせる追加装置を設計すること。
  3. 方法:センサーの選択(HC-SR04、JSN-SR04T、ToF)とその理由、取り付け角度とその計算、振動の対応づけとその式を示します。
  4. 結果:下のテストの結果を表とグラフで示し、うまくいかなかったケースも含めます。
  5. 結論と提言:プロジェクトが何を解決するのか、その限界(ガラス、柔らかい面、雨、使う人ではなく白杖の転倒を検知すること)、そして次のステップをはっきり述べます。
テストやり方記録する値
距離の精度壁から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つずつテストしてください。まずテスト用スケッチで距離、水の値、傾きが妥当かを確認し、それから完全なコードを書き込みます。MPU6050が応答しないときは、I2Cとは?の記事にあるスキャナーのスケッチでアドレスを見つけられます。振動が始まるとボードが勝手にリセットされる場合、原因はほぼ必ず電源です。Arduinoがリセットを繰り返すときの記事で、この問題をステップごとに解決できます。

スマート白杖は、距離の測定、触覚によるフィードバック、モーションセンサーによるイベント検知、電源の設計を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で距離を測定(シリアルモニタ表示)
#27初級

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

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

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

ブザーの使い方

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

4.7(89)
161
ブザーの周波数を調整
#14中級

ブザーの周波数を調整

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

4.6(69)
118
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駐車センサー(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
障害物回避ロボット
#81上級
プレミアム

障害物回避ロボット

自分で進む道を見つける、賢い車を作ってみませんか?障害物回避ロボットは、前面の超音波センサーで周囲を常に調べます。障害物を見つけると停止し、サーボモーターで左右を確認して、最も障害物の少ない方向を選んで進みます。自動運転の基礎を学ぶのにぴったりの入門プロジェクトです。

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

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

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

0.0(0)
9