Guides · 26 min read

Smart Blind Stick with Arduino: Obstacle, Water and Fall Alerts

How to build an Arduino smart blind stick (smart cane): HC-SR04 obstacle detection, vibration alerts, water sensor, MPU6050 fall detection, SOS and full code.

An Arduino smart blind stick warns a visually impaired person about obstacles that a white cane can't detect, and it's one of the most popular electronics projects for school assignments and science fairs. An HC-SR04 ultrasonic sensor mounted near the handle measures the distance to whatever is ahead; as the obstacle gets closer, a vibration motor in the handle pulses faster and harder, and if you like, a buzzer adds a short beep with each pulse. That way the user can feel obstacles above waist height, which the cane tip never touches, a few steps in advance.

In this guide you'll start with the basic circuit and then add, step by step, the extras that make the project genuinely usable: a puddle sensor near the tip of the cane, an algorithm that uses an MPU6050 to notice when the cane has fallen and sounds an SOS if nobody cancels it within 20 seconds, a power circuit that runs for more than 25 hours on a single 18650 cell, an enclosure designed with weight and ergonomics in mind, and an ESP32 + GPS extension that sends the location by SMS. At the end you'll also find concrete tips on how to test the project and present it at a science fair.

HC-SR04 ultrasonic distance sensor for a smart cane
The HC-SR04 sends a 40 kHz sound burst and calculates the distance from the time the echo takes to return. On a smart cane it watches for obstacles between 2 and 150 cm inside a narrow cone of about 15 degrees.

What is a smart cane and what does it do?

The white cane is the tool a blind person trusts most: every tap reveals the ground, the kerb, a step or a low obstacle ahead. But a cane only "sees" what its tip touches. Obstacles above waist height, such as a low tree branch, a sign sticking out over the pavement, a lorry's wing mirror, an open window or an air-conditioning unit jutting out of a wall, never touch the tip and hit the user's body or head instead. A smart cane is designed to close that gap: an electronic sensor scans the heights and distances the cane can't reach and passes the result to the user's hand as vibration.

Let's be clear about one thing from the start: a smart cane doesn't replace the white cane or orientation and mobility training, it complements them. The ultrasonic sensor has blind spots and batteries run flat, so the golden rule of the design is that the cane must still work exactly as before even when the electronics are completely switched off. International White Cane Safety Day, held every year on 15 October, is a great opportunity to show this project at your school.

How does a smart cane work?

The project consists of a single loop that runs hundreds of times per second and handles four independent jobs at once. Every 60 milliseconds the Arduino measures the distance, takes the median of the last three readings and adjusts the vibration rate accordingly. Every half second it reads the water sensor at the tip of the cane. Fifty times per second it reads the MPU6050's acceleration values to check whether the cane has fallen. And every thirty seconds it checks the battery voltage. The design has two output channels with separate jobs: vibration is only for obstacles, while sound signals special events such as water, a fall, SOS and a low battery. A short press of the button turns the obstacle beeps on or off if you like.

PartJobNote
Arduino Nano (Uno for prototyping)Measures, decides, drives the motor and buzzerThe Nano suits a cane because it's small and light
HC-SR04 ultrasonic sensorMeasures the distance to the obstacle ahead5 V, about 15 mA; JSN-SR04T for rainy use
Vibration motor moduleSignals the obstacle to the hand by vibrationReady-made module with a transistor and diode
Passive buzzerWater, fall, SOS and battery alertsPassive, so it can play different tones
Water level sensorWarns when the cane tip enters water1–2 cm above the tip, powered only while measuring
MPU6050 (GY-521)Detects that the cane has fallen and is lying flatI2C, address 0x68
Push buttonSound on/off, cancel alarm, SOS on a long pressPlaced under the thumb
18650 cell + TP4056 + MT3608Portable power, USB charging, boost to 5 VUse a protected TP4056 (with DW01)
2 × 100 kΩ resistorsDivider for measuring the battery voltageMidpoint goes to A0
Switch and enclosureOn/off and protection3D-printed or a small project box
Arduino Nano
Arduino Nano
Passive buzzer
Passive buzzer
Push button
Push button
18650 Li-ion battery
18650 Li-ion battery

Obstacle detection: how does the HC-SR04 measure, and where does it go?

When the HC-SR04 receives a 10-microsecond pulse on its TRIG pin, it sends eight sound waves at 40 kHz and holds the ECHO pin HIGH until the echo returns. At 20 °C sound travels about 343 metres per second; counting the trip there and back, roughly 58 microseconds pass for every centimetre. So the distance is simply cm = time (µs) / 58. The sensor sees inside a cone of about 15 degrees; 1.5 metres ahead that cone is about 40 cm wide. Because the sensor turns with the cane as you sweep it from side to side, it automatically scans a strip roughly one metre wide in front of you. You'll find the sensor's details in the HC-SR04 Distance Sensor article.

The most critical detail of the mounting is the angle. While walking, the cane isn't held upright but tilted forward at roughly 40–50 degrees from vertical. If you stick the sensor parallel to the cane, it points at the ground and constantly mistakes the floor for an obstacle. Fit the box close to the handle, about 75 cm above the ground, and aim the sensor slightly above horizontal. With the cane tilted at 45 degrees, the sensor axis must make a 60-degree angle with the shaft (measured towards the tip) for the sensor to point 15 degrees above horizontal. The easiest way to find the right angle is to hold the cane in your normal grip and rest a spirit-level app on your phone against the front of the box.

Sensor angle (above horizontal)Height scanned 1.5 m aheadTypical obstacles it catches
0°About 55–95 cmBenches, bins, the body of a parked car
15°About 95–135 cmA person's torso, bicycle handlebars, a lorry's load bed
25°About 120–170 cmSigns, tree branches, open window panes, wing mirrors

If you're starting with one sensor, 15–20 degrees is a good compromise: it catches obstacles at body height comfortably. Head-height obstacles, however, only enter the upper edge of the cone while they're still far away, and drop out of it as you get closer. If head height matters to you, dedicate a second HC-SR04 tilted 30–35 degrees upwards to it; because this sensor measures at a slant, keep its range at around 200 cm. Remember to trigger the two sensors one after the other with at least 60 ms between them, otherwise one hears the other's echo. For a detailed comparison with laser (ToF) and infrared sensors, see the distance sensor comparison article.

SensorRangeProsCons
HC-SR042–400 cmCheap, easy, wide coneNot waterproof, struggles with soft surfaces
JSN-SR04TAbout 25–450 cmWaterproof probe, works in the rainCan't see closer than 25 cm, more expensive
VL53L1X (laser ToF)Up to 4 mSmall, light, sharp beamRange drops in sunlight, 3.3 V I2C
Sharp GP2Y0A02 (infrared)20–150 cmAnalogue output, simple codeFooled by dark and shiny surfaces, draws about 33 mA
Know the ultrasonic sensor's blind spots: soft surfaces such as a thick coat, bushes or a curtain absorb the sound, while a glass door or a tilted panel reflects it away, so the obstacle may not show up at all. Thin poles 3–4 cm across are also noticed late because they're small inside the cone. That's why you should never present a smart cane as the only safety aid; it's an assistant that works alongside the white cane.

Vibration and beep rate based on distance

A blind person perceives their surroundings largely through sound: an approaching car, the hum of a crowd, the echo of the cane's tapping off a wall all help them find their way. A buzzer that beeps constantly masks those sounds rather than helping. So the main alert channel in this project is vibration, and the rule is simple: the closer the obstacle, the more frequent and the stronger the vibration pulses. Anything further than 150 cm is ignored, and if an obstacle is closer than 30 cm the motor vibrates continuously. In between, the pulse interval is calculated linearly with map(): interval = 120 + (distance − 30) × 6.5 ms.

Why 150 cm? Someone walking with a white cane covers about 1 metre per second. A 150 cm range gives them roughly 1.5 seconds before they reach the obstacle; since the reaction time to a tactile alert is 0.2–0.3 seconds, that's enough to stop or change direction. Stretch the range to 250 cm and on a busy pavement the cane vibrates non-stop, so after a while the user starts ignoring the alerts. Indoors 100 cm works well, outdoors 150–200 cm; all you need to change is the FAR_CM constant.

DistancePulse intervalVibration strength (PWM)Meaning for the user
Beyond 150 cmNo vibration0The path is clear
150 cm900 ms150There's an obstacle ahead
100 cm575 ms194Obstacle approaching, take care
60 cm315 ms229Slow down, get ready to change direction
40 cm185 ms247Very close
30 cm or closerContinuous255Stop
Arduino vibration motor module
Vibration motor module: next to the coin motor, about 10 mm across, sit the transistor that drives it and a flyback diode. The IN pin connects straight to an Arduino PWM pin, and the module carries the motor current.

The motor most often used in canes is a coin-type ERM motor like the one in mobile phones. It runs at about 3 V and draws around 60–80 mA. An Arduino pin, however, can supply at most 40 mA, with 20 mA as the safe limit, so the motor isn't connected to the pin directly but through a transistor. Ready-made modules already include that transistor and the diode that absorbs the reverse voltage when the motor stops; most modules run on 3.3–5 V. If you use a bare motor, an NPN transistor such as a BC337 or 2N2222, a 1 kΩ base resistor and a 1N4148 diode in reverse across the motor are enough; you can compare transistors in the Transistors tool.

Where you put the motor matters as much as the code. Place it inside the handle, where the palm or index finger rests, and fix it with thin rubber rather than foam, because foam soaks up the vibration. At full speed a coin motor vibrates at about 200 Hz, and our skin is most sensitive to vibration around 200–300 Hz, so even a small motor in the right place is easy to feel. Because ERM motors can't start spinning at very low PWM values, the weakest vibration in the code is kept at 150 out of 255. You'll find more on motor choice and controlling intensity with PWM in the vibration motor article.

Wiring diagram: Arduino Uno/Nano, ESP32 and Raspberry Pi Pico

The table below matches all the code in this article exactly. The pin numbers are the same on the Uno and Nano; on both boards the MPU6050's I2C bus is on A4 (SDA) and A5 (SCL). The vibration module's VCC goes to 5V, and its IN pin was deliberately put on D5: tone() uses Timer2, so PWM on D3 and D11 stops working while the buzzer is playing, whereas D5 belongs to Timer0 and isn't affected. You can see the Nano's pin map in the Pinout tool.

ConnectionArduino Uno / NanoESP32Raspberry Pi Pico
HC-SR04 VCC5VVIN (5 V)VBUS or the 5 V rail
HC-SR04 TRIGD9GPIO 18GP2
HC-SR04 ECHOD10 (direct)GPIO 19, via a 1 kΩ / 2 kΩ dividerGP3, via a 1 kΩ / 2 kΩ divider
Vibration module IND5 (PWM)GPIO 25GP15
Passive buzzer (+)D8GPIO 26GP16
Button (other leg to GND)D2GPIO 27GP14
Water sensor +D7GPIO 33GP22
Water sensor SA1GPIO 34 (ADC1)GP26 (ADC0)
MPU6050 SDAA4GPIO 21GP4
MPU6050 SCLA5GPIO 22GP5
MPU6050 VCC5V3V33V3
Battery sense (midpoint of the 100k/100k divider)A0GPIO 35 (ADC1)GP28 (ADC2)
NEO-6M GPS TX / RXNoneGPIO 16 / GPIO 17None
SIM800L TXD / RXDNoneGPIO 13 / GPIO 23None
The HC-SR04's ECHO pin outputs a 5 V pulse, but the ESP32's and Pico's pins run at 3.3 V and aren't 5 V tolerant. Build a voltage divider as ECHO → 1 kΩ → wire to the board → 2 kΩ → GND and about 3.3 V reaches the pin. On the ESP32, use ADC1 pins (GPIO 32–39) for analogue readings, because ADC2 doesn't work while Wi-Fi is on. On ESP32 boards with a WROVER module, GPIO 16 and 17 are connected to the PSRAM; if you have such a board, move the GPS to two other pins. All modules must share a common GND.

Step-by-step build: start with the basic cane

  1. Build the circuit on a breadboard first and leave fitting it to the cane until the very end. Seat the Arduino Nano in the middle of the breadboard and bring 5V and GND out to the side rails.
  2. Connect the HC-SR04's VCC and GND to the rails, TRIG to D9 and ECHO to D10.
  3. Connect the vibration module's VCC to 5V, its GND to GND and its IN (S on some modules) pin to D5. Plug the passive buzzer's positive leg into D8 and the other leg into GND.
  4. Upload the basic code below and open the Serial Monitor at 9600 baud. Slowly move your hand towards and away from the sensor; the distance on screen should change and the vibration should speed up and slow down with it.
  5. Hold the sensor 150, 100, 60 and 30 cm from a wall and compare the Serial Monitor value with a tape measure. If the difference is more than 1–2 cm, check the wiring and the power supply.
  6. Once everything works, add the water sensor, the MPU6050 and the button and move on to the full code.
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}

There's no long delay() anywhere in the code; every task runs on timestamps kept with millis(), so the vibration pulses never hold up the measurements. A 12 ms timeout was chosen for pulseIn(): since 12,000 / 58 ≈ 200 cm, echoes from further away are useless to us anyway, and a short timeout keeps the loop from stalling. If no echo returns, the function gives 999, which counts as "path clear". setVib() only writes to the motor when the value changes; because analogWrite() in the 2.x versions of the ESP32 core reconfigures the PWM unit on every call, this small detail lets the same code run smoothly there too.

Puddle sensor: an alert when the cane tip enters water

Puddles are among the hardest obstacles to notice with a white cane: when the tip goes into water, the sound and feel are hardly different from the ground, yet a few centimetres of water are enough to soak your shoes. For this we use a cheap water level sensor with interleaved copper traces. Some traces connect to the supply and some to the signal line; when water bridges them, the signal voltage rises and the analogRead() value increases. A dry sensor reads close to 0, while one whose end is in water usually gives a value of a few hundred on the Uno; the deeper it goes, the higher the value.

Fit the sensor at the very bottom of the cane with its lower edge about 1–2 cm above the tip and its traces perpendicular to the ground. That way nothing touches it on dry ground, but in a puddle deeper than 1–2 cm the traces get wet. The known weakness of these sensors is that the copper traces corrode quickly through electrolysis when they're powered all the time. The fix in the code is to connect the sensor's + pin to D7 rather than directly to 5V and power it only for the 10 ms of the measurement. With one reading every half second, the sensor is energised just 2% of the time. You'll find how it works in the water level sensor article and how it differs from a rain board in the rain sensor article.

Arduino water level sensor
Water level sensor: when water bridges the interleaved copper traces, the voltage on the S pin rises. On a smart cane it sits 1–2 cm above the tip and is powered only at the moment of measurement.
Coat the trace-free upper part of the sensor and its connector with hot glue or nail varnish; water should only touch the sensing traces. Don't leave the cable dangling outside the cane: run it through the inside of an aluminium cane or wrap it round the shaft inside heat-shrink tubing. For a tougher solution, fit two stainless steel screws 1 cm apart just above the tip: one goes to D7, the other to A1, which is pulled to GND through a 100 kΩ resistor. When water bridges the two screws, A1 rises; the code stays the same, you just measure WATER_LIMIT again.

Fall detection with the MPU6050

The MPU6050 is a motion sensor with a three-axis accelerometer and a three-axis gyroscope on a single chip, connected over I2C. The GY-521 board has a regulator on it, so it can run on 5 V; its default address is 0x68. In this project we only use the accelerometer and talk to four registers directly instead of using a library: writing 0 to 0x6B wakes the chip from sleep, 0x1A switches on the built-in 44 Hz filter, writing 0x10 to 0x1C raises the measuring range to ±8 g, and we read the X, Y and Z accelerations from the six bytes starting at 0x3B. The default ±2 g range easily saturates the moment the cane hits the ground, hiding the real size of the impact. In the ±8 g range, every 1 g corresponds to 4096 raw counts. To get to know the sensor better, have a look at the MPU6050 accelerometer and gyroscope article.

MPU6050 accelerometer and gyroscope module (GY-521)
MPU6050 (GY-521): a three-axis accelerometer and gyroscope. It's fixed inside the box with the X arrow printed on the board running along the cane shaft.

With every reading the code calculates two quantities. The first is the total acceleration: g = √(ax² + ay² + az²). While the cane is held still or lies on the ground this is about 1 g; in a hard impact it shoots up to several g. The second is the tilt, the angle between the cane and the vertical. If you mount the board with its X arrow along the shaft, the tilt is atan2(√(ay² + az²), |ax|): it reads 0 degrees with the cane upright, 40–55 while walking and 85–90 when it lies on the ground. Systems that detect human falls first look for a brief free fall (a moment close to 0 g); but a toppling cane moves like a stick rotating around its tip, so a sensor near the handle often never sees that moment. That's why our algorithm looks not for free fall but for an impact followed by a flat, motionless position.

StageConditionDurationWhat happens?
WALKTotal acceleration exceeds 2.5 gUnlimitedMoves to SETTLE; obstacle alerts keep running
SETTLEWaits for the shaking to stop1 sMoves to CHECK
CHECKTilt must stay above 70° and acceleration between 0.8 and 1.2 g2 sBack to WALK if the condition breaks, ALARM if it holds
ALARMA beep and vibration every second so the cane can be found20 sCancelled by a button press or by picking up the cane (tilt below 60°)
SOS... --- ... in Morse code with sound, LED and vibrationUntil cancelledA short press stops it; the ESP32 sends an SMS

This sequence prevents most false alarms. Tapping the cane on the ground while walking can also produce short impacts above 2.5 g, but a second later the cane is still in the hand and tilted at 40–55 degrees, so the check stage is cancelled immediately; the user won't even notice. When the cane falls and stays flat, though, the alarm starts after about 3 seconds, and the beep every second lets a user who dropped their cane find it by sound. If the user picks the cane up, the alarm stops by itself. If nobody reacts for 20 seconds, the code assumes it may be a real fall and starts the SOS. Holding the button for 2 seconds also starts an SOS manually at any time.

This method detects that the cane has fallen, not the user. If the person keeps hold of the cane while falling, or the cane ends up leaning against a wall, the alarm won't trigger; so always keep the manual SOS button and explain this limitation clearly in your presentation. To measure a body fall directly, the sensor needs to be in a separate device worn on the belt or the wrist.

Test and calibration code

Thresholds differ slightly for every cane, every surface and every sensor. Before moving on to the main code, upload the sketch below: every quarter of a second it prints the distance, the raw water sensor value, the highest acceleration in the last quarter second and the tilt to the Serial Monitor. Writing the values down in a table also fills in the results section of your project report.

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. Measure the water sensor dry and with its end dipped 1 cm into a glass of water. If it reads, say, 0–20 dry and 520 wet, choose the midpoint of 270–300 for WATER_LIMIT.
  2. Hold the cane in your normal grip and sweep it from side to side as if walking. Note the tilt; it usually stays between 40 and 55 degrees and should be below PICKUP_DEG (60).
  3. Lay the cane flat on the floor. The tilt should read 85–90 and the acceleration about 0.95–1.05 g, which confirms LYING_DEG (70) and the 0.8–1.2 g window.
  4. Tap the cane on the ground as usual while walking and watch the peak g values. Then drop the cane onto a mat 10 times. Set IMPACT_G below the drop values; even if taps exceed this threshold, the tilt check filters them out.
  5. Take 10 readings each at 30, 60, 100 and 150 cm from a wall and compare the average with a tape measure.

Full code: obstacles, water, falls, SOS and low-battery warning

In the full code the four tasks run in the same loop without holding each other up, and fall detection is a small state machine built with enum and switch-case. The button does three jobs: while walking, a short press turns the obstacle beeps on or off (vibration always stays on), during an alarm or SOS a short press cancels it, and a 2-second long press starts an SOS at any time. A high beep means sound on, a low beep means sound off.

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}
Function or settingWhat does it do?
pingCm()Takes one reading; returns 999 if no echo arrives within 12 ms
median3()Takes the median of the last three readings, discarding an odd bad value
readMpu()Reads the six bytes of acceleration data and calculates total acceleration and tilt
fallUpdate()Runs the WALK → SETTLE → CHECK → ALARM → SOS state machine
waterUpdate()Powers the water sensor for 10 ms, reads it and starts an alert on the change from dry to wet
walkOutput()Produces vibration according to distance and the water alert as a two-tone sound
sosOutput()Plays the SOS_PATTERN array step by step
readButton()Tells short (1) and long (2) presses apart and ignores bounces shorter than 30 ms
IMPACT_G, LYING_DEG, CANCEL_MSFall sensitivity and cancel time; tune them with the test sketch

The SOS pattern follows the rules of Morse code: a dot is 1 unit, a dash 3 units, with 1 unit between signals, 3 between letters and 7 between repeats. With a 150 ms unit, one SOS cycle is 34 units, or about 5.1 seconds. The 3 kHz tone was chosen on purpose: the human ear is most sensitive between 2 and 4 kHz, and small passive buzzers are loudest in that range too. For the difference between active and passive buzzers, see the What Is a Buzzer? article.

Battery, power and running time

A smart cane is used outdoors all day, so the power design matters as much as the sensors. Let's estimate the average consumption: the Nano draws about 20 mA, the HC-SR04 15 mA and the MPU6050 4 mA. The vibration motor draws around 70 mA, but only during pulses; on a typical walk its average is about 10 mA. The buzzer and water sensor add a few mA. That's roughly 50 mA in total, or 0.25 W at 5 V. A 2600 mAh 18650 cell holds about 9.6 Wh at 3.7 V; through an 85% efficient boost converter about 8 Wh reaches the circuit. 8 / 0.25 = 32 hours; allowing for the fact that you can't use the whole battery, a realistic figure is 25–30 hours. Someone who uses it two hours a day charges the cane about every two weeks. You can do your own calculation with the Battery life tool and read about the difference between mAh and Wh in the mAh and Wh article.

Power sourceWeightApprox. running time (50 mA)Verdict
9 V PP3 battery (about 500 mAh), into VIN45 g8–10 hoursNot recommended: voltage sags when the motor runs, not rechargeable
4 × AA NiMH (2000 mAh), into the 5V pinAbout 120 g35–40 hoursHeavy; reaches 5.6 V when fully charged, pushing the limit
1 × 18650 + TP4056 + MT3608About 55 g25–30 hoursRecommended: light, USB charging, protected
2 × 18650 in series (7.4 V), into VINAbout 105 g45–50 hoursLong-lasting but heavy, needs a separate charging circuit
Power bank (10,000 mAh)200 g or moreSeveral daysHeavy; most power banks switch off at low current
TP4056 Li-ion charging module
TP4056 charging module: charges the 18650 cell from USB up to 4.2 V at 1 A. The version with the DW01 protection chip also guards the cell against over-discharge and short circuits; the load connects to OUT+ and OUT−.

The recommended power chain is as follows: the 18650 cell connects to the B+ and B− terminals of the TP4056 module. The line from the module's OUT+ goes through a slide switch to the input of the MT3608 boost module. The MT3608's output, set to 5.0 V, powers the Nano's 5V pin, the HC-SR04 and the vibration module. To measure the battery voltage, a divider of two 100 kΩ resistors is connected right after the switch with its midpoint on A0; because it sits after the switch, it doesn't drain the battery while the cane is off. The DW01 chip cuts the cell off at about 2.4–2.5 V, but the code warns you much earlier by playing a low beep every 30 seconds below 3.4 V. More details in the TP4056 charging module article.

The MT3608's output can be set as high as 28 V with its trimmer, and the module may arrive from the factory set to any value. Before connecting it to the Nano, measure the output with a multimeter and turn the trimmer until it reads 5.0 V; a wrongly set boost converter destroys the board in seconds. If you're powering the board from USB alone during tests, A0 is left floating, the battery reading is random and you may hear the low-battery beep; that's normal.

Enclosure, weight and ergonomics

Most folding white canes weigh 200–300 g, and the user sweeps them from side to side with their wrist for hours. Every gram you add is multiplied by its distance from the hand and loads the wrist. Fit a 150 g box 10 cm below the handle and the extra torque on the wrist is 150 × 10 = 1.5 kg·cm; the same box 60 cm lower would make it 9 kg·cm, six times as much. So gather all the electronics in one box and place it right below the handle; only the water sensor and its thin cable run down to the tip. Aim to keep the total added weight under 150 g.

PartApprox. weight
Arduino Nano7 g
HC-SR049 g
MPU6050 (GY-521)2 g
Vibration module + passive buzzer5 g
Water level sensor and cable8 g
18650 cell45–48 g
TP4056 + MT3608 + switch8 g
3D-printed box and clamps40–50 g
Wires, screws, Velcro strap10–15 g
TotalAbout 135–150 g
  • Material: Print the box in PETG or ABS; PLA can soften and warp in a car left in the sun. A wall thickness of 1.6–2 mm is enough.
  • Mounting: Fix the box to the cane with two hose clamps or a rubber-lined Velcro strap. On folding canes the box must sit above the first folding joint, otherwise the cane won't fold.
  • Sensor window: The HC-SR04's two cylinders should stick out of the front of the box; don't put thin plastic or mesh in front of them, because ultrasound won't pass through.
  • Rain: Drill a small drainage hole in the bottom of the box and put a sachet of silica gel inside. If you live somewhere with frequent rain, use the waterproof JSN-SR04T probe instead of the HC-SR04.
  • Button: Choose a large button with a clear click and place it where the thumb naturally rests on the handle. Add a 2 mm raised rim around it so it isn't pressed by accident.
  • On/off: The switch position should be recognisable by touch; the power-on beep also confirms that the device is running.
  • Charging port: The TP4056's USB socket should be reachable from the underside of the box and closed with a rubber plug when not in use.
  • Visibility: Reflective tape on the sides of the box helps drivers notice the cane when the user crosses the road in the evening.

Sending the location with ESP32 + GPS

With an Arduino, the cane can only alert people nearby with sound during an SOS. With an ESP32 and a GPS module, however, the cane's location can be sent to a relative as a message. By default the NEO-6M module outputs NMEA sentences at 9600 baud, with one position fix per second; the TinyGPSPlus library parses them and gives you latitude, longitude and the number of satellites. According to the datasheet, the first fix from a cold start takes about 27 seconds; in practice, outdoors it takes anywhere from 30 seconds to a few minutes, and indoors there's often no fix at all. The small backup battery on the module keeps the last satellite data, so on later start-ups a fix arrives within a few seconds. Details in the NEO-6M GPS module article.

NEO-6M GPS module with its ceramic antenna
NEO-6M GPS module: the ceramic antenna needs a view of the sky, so it goes on the top of the box, away from metal parts. The default baud rate is 9600.

There are two ways to send the message. For a school demo, the easiest is to connect the ESP32 to the personal hotspot on the user's phone and send the message with a Telegram bot. For a cane that really works on the street, a SIM800L GSM module, which doesn't depend on the phone, is a better fit: it sends an SMS directly with the SIM card inside it. The SIM800L needs 3.4–4.4 V and draws current spikes of up to 2 A while transmitting, so power it straight from the 18650 cell rather than from the boost converter, and put a 1000 µF capacitor across its supply pins. This module only works on 2G networks; in countries where 2G has been switched off you'll need a 4G module such as the A7670 or SIM7600. If you'd like everything on one board, you can use a 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}

The code uses the same state machine as the Arduino version; the difference is in the SOS. As soon as the SOS starts, sendSms() sends the SIM800L the commands AT, AT+CMGF=1 for text mode and AT+CMGS="number" in turn, waits for the ">" prompt, writes the message and sends it with Ctrl+Z (26). The location appears in the message as a map link, which opens the maps app when tapped on a phone. If the GPS hasn't got a fix yet, the message says so, and while the SOS continues the location is sent again every 5 minutes. The SMS text deliberately avoids Turkish letters: characters such as ş, ğ and ı aren't in the standard GSM alphabet and show up garbled on some phones. While sending, the code may wait for up to 30 seconds; after power-up the SIM800L also needs 10–30 seconds to register on the network.

Smart cane with the Raspberry Pi Pico (MicroPython)

On the Raspberry Pi Pico the same logic is built with MicroPython. time_pulse_us() works like pulseIn() on the Arduino and returns a negative value if no echo arrives. PWM outputs are set with the 16-bit duty_u16(); for the vibration motor, 65535 means full power. The MPU6050's six bytes are read in one go with i2c.readfrom_mem() and converted to signed numbers by subtracting 65536 from values above 32767. The Pico's pins are 3.3 V too, so the ECHO divider is essential here as well. When running on battery, feed the MT3608's 5 V output into the VSYS pin; the HC-SR04 is powered from the same 5 V rail. With the Wi-Fi-equipped Pico W you can also show the cane's status on a web page.

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)

Tips for a science fair or competition presentation

The smart cane is a familiar sight at science fairs and school competitions (you'll find more ideas in our science fair project ideas article), so what impresses the judges isn't that the cane vibrates but how well you define the problem and how honestly you measure your solution. The strongest projects begin with a conversation with a visually impaired person or an association: hearing from them which obstacles are a real problem, whether they prefer vibration or sound and how heavy the cane can be gives your project both direction and a powerful story.

  1. Problem: The white cane can't detect obstacles above waist height, and puddles go unnoticed. Explain it with concrete examples from your interviews.
  2. Aim: To design an add-on that signals obstacles at body and head height by vibration at least 1 second in advance, without changing the cane's weight or the way it's used too much.
  3. Method: Show your sensor choice (HC-SR04, JSN-SR04T, ToF) with your reasons, the mounting angle with its calculation and the vibration mapping with its formula.
  4. Results: Present the results of the tests below in tables and charts, and include the cases where it failed.
  5. Conclusion and recommendations: State clearly what the project solves, its limits (glass, soft surfaces, rain, detecting the cane's fall rather than the user's) and the next step.
TestHow to do itValue to record
Distance accuracy10 readings each at 30, 60, 100 and 150 cm from a wallAverage error (cm) and largest deviation
Obstacle typesWall, person, 5 cm pole, bush, glass door, fabric curtainHow many times out of 10 each obstacle is detected
Response timeSlow-motion phone video: the obstacle appearing and the first vibrationDelay in milliseconds
Water detectionDip the tip into 0.5, 1, 2 and 3 cm of waterFrom which depth it warns
Fall detection20 drops and 100 taps on the groundNumber of correct and false alarms
Battery lifeRun it from a full charge until it switches offTime in hours and final voltage
Weight and balanceWeigh it on kitchen scales, measure the distance from the handleAdded weight (g) and torque (kg·cm)
Run user tests with blindfolded volunteers in an empty, safe corridor, always with a helper beside them; never test near traffic. Prepare a consent form for every test involving people and check which ethics documents the competition rules require. The judges will very likely ask "does it see glass doors?", "what happens in the rain?" and "what happens when the battery runs out?"; prepare your answers in advance using the data in your test tables.

Ideas for improvements

  • Step and hole warning: A second HC-SR04 pointing down and forwards normally sees the ground at a constant distance; if the reading suddenly gets longer, there's a step down or a hole ahead.
  • Voice messages: Use a DFPlayer Mini to play recorded phrases such as "obstacle ahead", "water" or "battery low" in an earphone instead of beeps.
  • Phone app: Use the ESP32's Bluetooth BLE to show the battery level and settings on a phone and change the range from there.
  • Direction: Put two separate motors on the left and right of the handle and use vibration to show which side the obstacle is on.
  • Sleep mode: Switch the sensors off when the MPU6050 sees that the cane hasn't moved for minutes; with ESP32 deep sleep the battery life multiplies.
  • Night visibility: Use an LDR to flash an LED at the tip of the cane after dark, so drivers notice the user from far away.

Common mistakes and how to fix them

SymptomLikely causeFix
The cane vibrates non-stopThe sensor is pointing at the ground or at the cane itselfRaise the mounting angle and keep the space in front of the sensor clear
Occasional vibration for no reasonAn odd bad echo or another ultrasonic source nearbyThe median filter is already there; if needed, take the median of 5 readings
The motor doesn't spin at long distancesThe PWM value is below the motor's start thresholdKeep the minimum strength above 150 and fix the motor with rubber
The Arduino restarts when the motor runsThe motor current pulls the supply down, or there's no diodeUse a ready-made module and add a 100 µF capacitor to the supply
The MPU6050 always reads 0 or a fixed valueSDA and SCL swapped, address is 0x69, or the chip is asleepFind the address with an I2C scanner and check that 0 is written to register 0x6B
False alarms from taps on the groundIMPACT_G is low and the tilt threshold is close to the walking angleMeasure the tilt with the test sketch and raise LYING_DEG to 75
The water alert never stops or never soundsWrong threshold or oxidised tracesMeasure the dry and wet values again and replace the sensor if needed
The buzzer is silent or always plays the same toneAn active buzzer is fittedUse a passive buzzer for tone()
The SMS isn't sentThe SIM800L isn't registered on the network, the supply is weak or the SIM's PIN is enabledCheck that the module's LED blinks every 3 seconds, disable the PIN and power the module from the 18650
When troubleshooting, test the parts one at a time: first use the test sketch to check that the distance, water value and tilt make sense, then upload the full code. If the MPU6050 doesn't respond, the scanner sketch in the What is I2C article finds its address. If the board resets itself when the vibration starts, the cause is almost always the power supply; the Arduino keeps resetting article solves this problem step by step.

A smart cane brings distance measurement, haptic feedback, event detection with a motion sensor and power design together in a single project, and because it addresses a real need, it's one of the most meaningful Arduino projects you can build. Start with the basic code in this guide, fill in your test tables, then add the water, fall and GPS layers one by one. In the BlueGrays app you'll find distance measurement with the HC-SR04, a parking sensor, sound with a buzzer and ESP32 notification projects step by step with circuit diagrams and ready-to-use code, so you can try out each part of your cane on its own first.

Frequently asked questions

How does a smart blind stick work?

An HC-SR04 ultrasonic sensor mounted near the handle sends out sound waves and calculates the distance to the obstacle ahead from the time the echo takes to return. Based on that distance, the Arduino drives a vibration motor in the handle: the closer the obstacle, the faster and stronger the vibration. In more advanced versions, a water sensor at the tip detects puddles and an MPU6050 detects when the cane falls.

What components do I need for an Arduino smart cane?

For the basic version, an Arduino Nano, an HC-SR04 ultrasonic sensor, a vibration motor module, a passive buzzer and jumper wires are enough. The full version adds a water level sensor, an MPU6050, a button, an 18650 cell with TP4056 charging and MT3608 boost modules. To send the location you need an ESP32, a NEO-6M GPS and a SIM800L GSM module.

How far can a smart cane detect obstacles?

The HC-SR04 can measure from 2 to 400 cm, but a cane usually uses a 150 cm limit. For someone walking at about 1 metre per second, that means a 1.5-second warning before reaching the obstacle. Making the range much longer leads to constant vibration in busy places and to users ignoring the alerts.

How does a smart cane know it has fallen?

The MPU6050 accelerometer is read 50 times per second. If, after an impact above 2.5 g, the cane stays flat and motionless for 2 seconds, it's considered to have fallen and a locator beep starts. If the cane isn't picked up or the button isn't pressed within 20 seconds, an SOS sounds; the ESP32 version sends the location by SMS at that moment.

How long does an Arduino smart cane run on a battery?

The circuit draws around 50 mA on average. A single 2600 mAh 18650 cell with TP4056 charging and MT3608 boost modules runs it for about 25–30 hours, which works out at roughly two weeks with two hours of use a day. A 9 V PP3 battery only lasts 8–10 hours and isn't recommended because its voltage sags when the motor runs.

Can I add GPS location sharing to a smart cane?

Yes. If you use an ESP32 instead of an Arduino and add a NEO-6M GPS module, you can read the cane's latitude and longitude. With a SIM800L GSM module, the location goes to a relative by SMS as a map link during an SOS; if there's Wi-Fi, a Telegram bot works too. GPS works outdoors but usually can't get a fix indoors.

See all projects
HC-SR04 Distance Measurement (Serial Display)
#27Beginner

HC-SR04 Distance Measurement (Serial Display)

The HC-SR04 sensor works just like a bat finding its way in the dark: it sends out a sound you cannot hear, listens for the echo and measures how far away the object in front of it is. The Arduino wri

4.6(46)
71
Using a Buzzer
#13Beginner

Using a Buzzer

Ever wondered how a doorbell or an alarm makes its sound? The Arduino makes a small buzzer beep at short intervals: beep, pause, beep, pause. You see how sound is made with electricity and take the fi

4.7(89)
161
Frequency Setting with Buzzer
#14Intermediate

Frequency Setting with Buzzer

You control the sound of a buzzer with four buttons: one starts the sound, one silences it, and the other two make it higher or lower. Hold the up or down button and the pitch changes step by step, sl

4.6(69)
118
Parking Sensor with HC-SR04
#28Intermediate
Premium

Parking Sensor with HC-SR04

A mini version of a car's parking sensor: the closer something gets, the faster the beeps. The distance sensor measures the object in front of it; if it is more than 20 centimetres away the green LED

4.8(64)
85
ESP32 HC-SR04 Ultrasonic Distance Sensor
#116Intermediate

ESP32 HC-SR04 Ultrasonic Distance Sensor

The HC-SR04 ultrasonic sensor calculates distance by measuring the time it takes for the sound wave it emits to bounce off an object and return (Time of Flight). In this project, we will send a 10-microsecond pulse from the ESP32 to the sensor's TRIG pin and read the echo duration from the ECHO pin using pulseIn(). We will display the calculated distance in centimeters on a 16x2 I2C LCD. Since the HC-SR04 operates at 5V, a voltage divider is required on the ECHO pin—otherwise, the ESP32’s

4.9(28)
45
ESP32 Parking Sensor (HC-SR04 + 5-LED Bar + Buzzer + OLED)
#142Advanced
Premium

ESP32 Parking Sensor (HC-SR04 + 5-LED Bar + Buzzer + OLED)

The ESP32 version of the car parking sensor! We use the HC-SR04 ultrasonic sensor to measure the distance to the object in front of it. As the distance decreases, the 5-LED bar fills up (green → yellow → red) and the buzzer beep interval shortens. When the object gets very close (under 10 cm), a continuous alarm sounds and all LEDs light up. The SSD1306 OLED display shows the current distance in cm in large font, a status message (STOP / CAUTION / CLOSE / STOP! / DANGER), and a simulation of the

0.0(0)
14
Obstacle Avoidance Robot
#81Advanced
Premium

Obstacle Avoidance Robot

How about building a smart vehicle that finds its own way? The Obstacle Avoidance Robot constantly scans its surroundings thanks to the ultrasonic sensor in front of it. When it encounters an obstacle

5.0(38)
49
ESP32 Motion Alarm System (PIR + Buzzer + LED + Telegram)
#145Advanced
Premium

ESP32 Motion Alarm System (PIR + Buzzer + LED + Telegram)

A smart security alarm system that activates the buzzer and LED when the PIR sensor detects motion, while simultaneously sending an instant notification to the owner via Telegram. It includes a 30-second cooldown to limit the frequency of repeated alarms; this mechanism does not validate PIR detections or eliminate false triggers. Suitable for home security, garage monitoring, warehouses, or workrooms. With ESP32-CAM, photo notifications can be added; with the classic ESP32 DevKit V1, text notif

0.0(0)
9