Smarter Blindenstock mit Arduino: Hindernisse, Wasser, Stürze
Smarten Blindenstock mit Arduino bauen: HC-SR04-Hinderniserkennung, Vibrationsalarm, Wassersensor, MPU6050-Sturzerkennung, SOS und kompletter Code.
Ein smarter Blindenstock mit Arduino warnt blinde und sehbehinderte Menschen vor Hindernissen, die der weiße Langstock nicht erfassen kann, und gehört zu den beliebtesten Elektronikprojekten für Schule, Jugend forscht und Wissenschaftsmessen. Ein HC-SR04-Ultraschallsensor in der Nähe des Griffs misst den Abstand zum Hindernis; je näher es kommt, desto schneller und kräftiger pulsiert ein Vibrationsmotor im Griff, und wenn du willst, gibt ein Buzzer bei jedem Impuls einen kurzen Piepton. So spürt die Person Hindernisse oberhalb der Hüfte, die die Stockspitze nie berührt, schon einige Schritte im Voraus.
In dieser Anleitung beginnst du mit der Grundschaltung und baust Schritt für Schritt die Erweiterungen ein, die das Projekt wirklich alltagstauglich machen: einen Pfützensensor nahe der Stockspitze, einen Algorithmus, der mit einem MPU6050 erkennt, dass der Stock umgefallen ist, und SOS gibt, wenn niemand innerhalb von 20 Sekunden abbricht, eine Stromversorgung, die mit einer einzigen 18650-Zelle über 25 Stunden läuft, ein Gehäuse, das Gewicht und Ergonomie berücksichtigt, und eine ESP32 + GPS-Erweiterung, die den Standort per SMS verschickt. Am Ende findest du außerdem konkrete Tipps, wie du das Projekt testest und auf einer Wissenschaftsmesse präsentierst.

Was ist ein smarter Blindenstock und wozu dient er?
Der weiße Stock ist das Hilfsmittel, dem blinde Menschen am meisten vertrauen: Mit jedem Tippen meldet er den Boden, die Bordsteinkante, eine Stufe oder ein niedriges Hindernis. Aber ein Stock "sieht" nur, was seine Spitze berührt. Hindernisse oberhalb der Hüfte, etwa ein tief hängender Ast, ein Schild, das auf den Gehweg ragt, der Außenspiegel eines Lkw, ein offenes Fenster oder ein Klimagerät an der Hauswand, berühren die Spitze nie und treffen stattdessen Körper oder Kopf. Ein smarter Blindenstock soll diese Lücke schließen: Ein elektronischer Sensor tastet die Höhen und Entfernungen ab, die der Stock nicht erreicht, und gibt das Ergebnis als Vibration an die Hand weiter.
Eines sei gleich zu Beginn klargestellt: Ein smarter Stock ersetzt weder den Langstock noch das Orientierungs- und Mobilitätstraining, er ergänzt sie. Der Ultraschallsensor hat tote Winkel und Akkus werden leer; deshalb lautet die Grundregel des Designs, dass der Stock auch bei komplett ausgeschalteter Elektronik genauso funktioniert wie vorher. Der Internationale Tag des weißen Stockes am 15. Oktober ist eine gute Gelegenheit, das Projekt an deiner Schule vorzustellen.
Wie funktioniert ein smarter Blindenstock?
Das Projekt besteht aus einer einzigen Schleife, die hunderte Male pro Sekunde durchläuft und vier unabhängige Aufgaben gleichzeitig erledigt. Alle 60 Millisekunden misst der Arduino den Abstand, bildet den Median der letzten drei Messungen und passt die Vibrationsfrequenz an. Jede halbe Sekunde liest er den Wassersensor an der Stockspitze. Fünfzigmal pro Sekunde holt er die Beschleunigungswerte des MPU6050 und prüft, ob der Stock umgefallen ist. Und alle dreißig Sekunden kontrolliert er die Akkuspannung. Es gibt zwei Ausgabekanäle mit getrennten Aufgaben: Vibration ist nur für Hindernisse da, Töne melden besondere Ereignisse wie Wasser, Sturz, SOS und schwachen Akku. Mit einem kurzen Tastendruck kannst du die Hindernis-Pieptöne ein- oder ausschalten.
| Bauteil | Aufgabe | Hinweis |
|---|---|---|
| Arduino Nano (zum Testen Uno) | Misst, entscheidet, steuert Motor und Buzzer | Der Nano passt zum Stock, weil er klein und leicht ist |
| HC-SR04-Ultraschallsensor | Misst den Abstand zum Hindernis | 5 V, etwa 15 mA; bei Regen JSN-SR04T |
| Vibrationsmotor-Modul | Meldet Hindernisse per Vibration an die Hand | Fertiges Modul mit Transistor und Diode |
| Passiver Buzzer | Wasser-, Sturz-, SOS- und Akkuwarnungen | Passiv, damit er verschiedene Töne spielen kann |
| Wasserstandssensor | Warnt, wenn die Stockspitze ins Wasser taucht | 1–2 cm über der Spitze, nur beim Messen versorgt |
| MPU6050 (GY-521) | Erkennt, dass der Stock umgefallen ist und flach liegt | I2C, Adresse 0x68 |
| Taster | Ton an/aus, Alarm abbrechen, SOS bei langem Druck | Unter dem Daumen platziert |
| 18650-Zelle + TP4056 + MT3608 | Mobile Versorgung, USB-Laden, Hochsetzen auf 5 V | Geschütztes TP4056-Modul (mit DW01) verwenden |
| 2 × 100-kΩ-Widerstand | Spannungsteiler zum Messen der Akkuspannung | Mittelpunkt an A0 |
| Schalter und Gehäuse | Ein/Aus und Schutz | 3D-Druck oder kleines Projektgehäuse |




Hinderniserkennung: Wie misst der HC-SR04 und wo wird er montiert?
Bekommt der HC-SR04 an TRIG einen 10-Mikrosekunden-Impuls, sendet er acht Schallwellen mit 40 kHz aus und hält ECHO auf HIGH, bis das Echo zurückkommt. Schall legt bei 20 °C etwa 343 Meter pro Sekunde zurück; für Hin- und Rückweg vergehen pro Zentimeter rund 58 Mikrosekunden. Der Abstand ergibt sich also einfach aus cm = Zeit (µs) / 58. Der Sensor sieht in einem Kegel von etwa 15 Grad; 1,5 Meter vor dir ist dieser Kegel rund 40 cm breit. Da sich der Sensor beim Pendeln mit dem Stock dreht, tastet er automatisch einen etwa einen Meter breiten Streifen vor dir ab. Details zum Sensor findest du im Artikel HC-SR04 Ultraschallsensor.
Das wichtigste Detail bei der Montage ist der Winkel. Beim Gehen wird der Stock nicht senkrecht, sondern etwa 40–50 Grad zur Senkrechten nach vorne geneigt gehalten. Klebst du den Sensor parallel zum Stock, zeigt er auf den Boden und hält ihn ständig für ein Hindernis. Befestige das Gehäuse nahe am Griff, etwa 75 cm über dem Boden, und richte den Sensor leicht über die Waagerechte. Bei 45 Grad Neigung muss die Sensorachse mit dem Stockschaft (zur Spitze hin gemessen) 60 Grad bilden, damit der Sensor 15 Grad über die Waagerechte zeigt. Am einfachsten findest du den richtigen Winkel, indem du den Stock normal hältst und eine Wasserwaagen-App auf dem Handy an die Vorderseite des Gehäuses legst.
| Sensorwinkel (über der Waagerechten) | Abgetastete Höhe in 1,5 m Entfernung | Typische erkannte Hindernisse |
|---|---|---|
| 0° | Etwa 55–95 cm | Bänke, Mülltonnen, die Karosserie eines geparkten Autos |
| 15° | Etwa 95–135 cm | Oberkörper, Fahrradlenker, Lkw-Ladefläche |
| 25° | Etwa 120–170 cm | Schilder, Äste, offene Fensterflügel, Außenspiegel |
Wenn du mit einem Sensor startest, sind 15–20 Grad ein guter Kompromiss: Hindernisse in Körperhöhe erfasst er sicher. Hindernisse in Kopfhöhe geraten dagegen nur an den oberen Rand des Kegels, solange sie noch weit weg sind, und fallen beim Näherkommen aus ihm heraus. Ist dir die Kopfhöhe wichtig, widme ihr einen zweiten HC-SR04, der 30–35 Grad nach oben zeigt; weil dieser Sensor schräg misst, sollte seine Reichweite bei etwa 200 cm liegen. Löse die beiden Sensoren nacheinander mit mindestens 60 ms Abstand aus, sonst hört einer das Echo des anderen. Einen ausführlichen Vergleich mit Laser- (ToF) und Infrarotsensoren findest du im Artikel Abstandssensoren im Vergleich.
| Sensor | Reichweite | Vorteile | Nachteile |
|---|---|---|---|
| HC-SR04 | 2–400 cm | Günstig, einfach, breiter Kegel | Nicht wasserdicht, hat Probleme mit weichen Oberflächen |
| JSN-SR04T | Etwa 25–450 cm | Wasserdichter Messkopf, funktioniert bei Regen | Sieht nichts unter 25 cm, teurer |
| VL53L1X (Laser-ToF) | Bis 4 m | Klein, leicht, scharf gebündelt | Reichweite sinkt im Sonnenlicht, 3,3-V-I2C |
| Sharp GP2Y0A02 (Infrarot) | 20–150 cm | Analogausgang, einfacher Code | Lässt sich von dunklen und glänzenden Flächen täuschen, zieht etwa 33 mA |
Vibrations- und Piepfrequenz je nach Abstand
Blinde Menschen nehmen ihre Umgebung zu einem großen Teil über das Gehör wahr: ein herannahendes Auto, das Murmeln einer Menschenmenge, das Echo des Stocktippens von einer Wand weisen ihnen den Weg. Ein ständig piepender Buzzer überdeckt diese Geräusche eher, als dass er hilft. Der Hauptwarnkanal in diesem Projekt ist deshalb die Vibration, und die Regel ist einfach: Je näher das Hindernis, desto häufiger und stärker die Vibrationsimpulse. Alles, was weiter als 150 cm entfernt ist, wird ignoriert; ist ein Hindernis näher als 30 cm, vibriert der Motor ununterbrochen. Dazwischen wird der Impulsabstand mit map() linear berechnet: Impulsabstand = 120 + (Entfernung − 30) × 6,5 ms.
Warum 150 cm? Wer mit dem Langstock geht, kommt etwa 1 Meter pro Sekunde voran. Eine Reichweite von 150 cm verschafft also rund 1,5 Sekunden, bevor man das Hindernis erreicht; da die Reaktionszeit auf einen Tastreiz bei 0,2–0,3 Sekunden liegt, reicht das zum Anhalten oder Ausweichen. Erhöhst du die Reichweite auf 250 cm, vibriert der Stock auf einem belebten Gehweg ununterbrochen und die Warnungen werden irgendwann ignoriert. Drinnen funktionieren 100 cm gut, draußen 150–200 cm; dafür änderst du nur die Konstante FAR_CM.
| Abstand | Impulsabstand | Vibrationsstärke (PWM) | Bedeutung für den Nutzer |
|---|---|---|---|
| Über 150 cm | Keine Vibration | 0 | Der Weg ist frei |
| 150 cm | 900 ms | 150 | Vorne ist ein Hindernis |
| 100 cm | 575 ms | 194 | Hindernis kommt näher, aufpassen |
| 60 cm | 315 ms | 229 | Langsamer werden, Ausweichen vorbereiten |
| 40 cm | 185 ms | 247 | Sehr nah |
| 30 cm oder näher | Dauerhaft | 255 | Stehen bleiben |

In Blindenstöcken kommt meist ein scheibenförmiger ERM-Motor zum Einsatz, wie man ihn aus Handys kennt. Er läuft mit etwa 3 V und zieht rund 60–80 mA. Ein Arduino-Pin liefert aber höchstens 40 mA, sicher sind 20 mA; deshalb wird der Motor nicht direkt, sondern über einen Transistor angeschlossen. Fertige Module enthalten diesen Transistor bereits, ebenso die Diode, die die Gegenspannung beim Abschalten des Motors abbaut; die meisten Module laufen mit 3,3–5 V. Nimmst du einen nackten Motor, genügen ein NPN-Transistor wie BC337 oder 2N2222, ein 1-kΩ-Basiswiderstand und eine 1N4148-Diode antiparallel zum Motor; Transistoren vergleichen kannst du im Werkzeug Transistoren.
Wo der Motor sitzt, ist genauso wichtig wie der Code. Setze ihn in den Griff, dort, wo Handfläche oder Zeigefinger aufliegen, und befestige ihn mit dünnem Gummi statt mit Schaumstoff, denn Schaumstoff schluckt die Vibration. Bei voller Drehzahl vibriert ein Scheibenmotor mit etwa 200 Hz, und unsere Haut ist um 200–300 Hz am empfindlichsten für Vibrationen; selbst ein kleiner Motor an der richtigen Stelle ist also gut zu spüren. Weil ERM-Motoren bei sehr kleinen PWM-Werten nicht anlaufen, liegt die schwächste Vibration im Code bei 150 von 255. Mehr zur Motorwahl und zur Stärkeregelung per PWM findest du im Artikel Vibrationsmotor.
Schaltplan: Arduino Uno/Nano, ESP32 und Raspberry Pi Pico
Die folgende Tabelle stimmt exakt mit allen Codes in diesem Artikel überein. Uno und Nano haben dieselben Pinnummern; bei beiden liegt der I2C-Bus des MPU6050 an A4 (SDA) und A5 (SCL). VCC des Vibrationsmoduls kommt an 5V, und der IN-Pin liegt bewusst an D5: tone() nutzt Timer2, deshalb funktioniert PWM an D3 und D11 nicht, solange der Buzzer spielt, während D5 zu Timer0 gehört und nicht betroffen ist. Die Pinbelegung des Nano siehst du im Werkzeug Pinout.
| Verbindung | Arduino Uno / Nano | ESP32 | Raspberry Pi Pico |
|---|---|---|---|
| HC-SR04 VCC | 5V | VIN (5 V) | VBUS oder 5-V-Schiene |
| HC-SR04 TRIG | D9 | GPIO 18 | GP2 |
| HC-SR04 ECHO | D10 (direkt) | GPIO 19, über 1-kΩ-/2-kΩ-Teiler | GP3, über 1-kΩ-/2-kΩ-Teiler |
| Vibrationsmodul IN | D5 (PWM) | GPIO 25 | GP15 |
| Passiver Buzzer (+) | D8 | GPIO 26 | GP16 |
| Taster (anderes Bein an GND) | D2 | GPIO 27 | GP14 |
| Wassersensor + | D7 | GPIO 33 | GP22 |
| Wassersensor S | A1 | GPIO 34 (ADC1) | GP26 (ADC0) |
| MPU6050 SDA | A4 | GPIO 21 | GP4 |
| MPU6050 SCL | A5 | GPIO 22 | GP5 |
| MPU6050 VCC | 5V | 3V3 | 3V3 |
| Akkumessung (Mitte des 100k/100k-Teilers) | A0 | GPIO 35 (ADC1) | GP28 (ADC2) |
| NEO-6M GPS TX / RX | Keine | GPIO 16 / GPIO 17 | Keine |
| SIM800L TXD / RXD | Keine | GPIO 13 / GPIO 23 | Keine |
Schritt für Schritt: zuerst der einfache Stock
- Baue die Schaltung zuerst auf dem Steckbrett auf und montiere sie erst ganz am Ende am Stock. Setze den Arduino Nano in die Mitte des Steckbretts und führe 5V und GND auf die seitlichen Schienen.
- Verbinde VCC und GND des HC-SR04 mit den Schienen, TRIG mit D9 und ECHO mit D10.
- Schließe VCC des Vibrationsmoduls an 5V, GND an GND und den IN-Pin (bei manchen Modulen S) an D5 an. Stecke das Plusbein des passiven Buzzers an D8 und das andere an GND.
- Lade den Grundcode unten hoch und öffne den seriellen Monitor mit 9600 Baud. Bewege deine Hand langsam auf den Sensor zu und wieder weg; der angezeigte Abstand sollte sich ändern und die Vibration entsprechend schneller und langsamer werden.
- Halte den Sensor 150, 100, 60 und 30 cm vor eine Wand und vergleiche den Wert im seriellen Monitor mit dem Maßband. Weicht er um mehr als 1–2 cm ab, prüfe Verkabelung und Stromversorgung.
- Wenn alles läuft, ergänze Wassersensor, MPU6050 und Taster und wechsle zum vollständigen Code.
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}Im Code gibt es kein langes delay(); jede Aufgabe läuft über Zeitstempel mit millis(), sodass die Vibrationsimpulse die Messungen nie aufhalten. Für pulseIn() wurde ein Timeout von 12 ms gewählt: Da 12.000 / 58 ≈ 200 cm sind, nützen uns weiter entfernte Echos ohnehin nichts, und das kurze Timeout verhindert, dass die Schleife hängt. Kommt kein Echo zurück, liefert die Funktion 999, was als "Weg frei" gilt. setVib() schreibt nur dann an den Motor, wenn sich der Wert ändert; weil analogWrite() in den 2.x-Versionen des ESP32-Cores die PWM-Einheit bei jedem Aufruf neu konfiguriert, läuft derselbe Code dank dieses kleinen Details auch dort reibungslos.
Pfützensensor: Warnung, wenn die Stockspitze ins Wasser taucht
Pfützen gehören zu den Hindernissen, die man mit dem weißen Stock am schwersten bemerkt: Taucht die Spitze ins Wasser, klingt und fühlt sich das kaum anders an als der Boden, doch ein paar Zentimeter Wasser genügen für nasse Schuhe. Dafür nutzen wir einen günstigen Wasserstandssensor mit ineinandergreifenden Kupferbahnen. Ein Teil der Bahnen hängt an der Versorgung, ein Teil an der Signalleitung; verbindet Wasser sie, steigt die Signalspannung und der analogRead()-Wert nimmt zu. Trocken liest der Sensor fast 0, mit der Spitze im Wasser auf dem Uno meist einige Hundert; je tiefer er eintaucht, desto größer der Wert.
Montiere den Sensor ganz unten am Stock, mit der Unterkante etwa 1–2 cm über der Spitze und den Bahnen senkrecht zum Boden. So berührt ihn auf trockenem Boden nichts, in einer Pfütze tiefer als 1–2 cm werden die Bahnen aber nass. Die bekannte Schwäche dieser Sensoren: Bei Dauerversorgung korrodieren die Kupferbahnen durch Elektrolyse schnell. Die Lösung im Code ist, den +-Pin des Sensors nicht direkt an 5V, sondern an D7 zu legen und ihn nur während der 10 ms der Messung zu versorgen. Bei einer Messung pro halbe Sekunde steht der Sensor nur 2 % der Zeit unter Spannung. Wie er funktioniert, liest du im Artikel Wasserstandssensor, den Unterschied zur Regenplatine im Artikel Regensensor.

Sturzerkennung mit dem MPU6050
Der MPU6050 ist ein Bewegungssensor mit Dreiachsen-Beschleunigungssensor und Dreiachsen-Gyroskop auf einem Chip, angeschlossen über I2C. Die GY-521-Platine hat einen Spannungsregler und kann daher mit 5 V betrieben werden; die Standardadresse ist 0x68. In diesem Projekt nutzen wir nur den Beschleunigungssensor und sprechen statt mit einer Bibliothek direkt mit vier Registern: Eine 0 in 0x6B weckt den Chip aus dem Schlaf, 0x1A schaltet das eingebaute 44-Hz-Filter ein, 0x10 in 0x1C erweitert den Messbereich auf ±8 g, und aus den sechs Bytes ab 0x3B lesen wir die Beschleunigungen in X, Y und Z. Der Standardbereich von ±2 g ist beim Aufprall des Stocks schnell übersteuert, und die echte Stärke des Stoßes bleibt verborgen. Im Bereich ±8 g entspricht 1 g genau 4096 Rohwerten. Mehr über den Sensor erfährst du im Artikel MPU6050 Beschleunigungssensor und Gyroskop.

Bei jeder Messung berechnet der Code zwei Größen. Die erste ist die Gesamtbeschleunigung: g = √(ax² + ay² + az²). Hält man den Stock ruhig oder liegt er am Boden, beträgt sie etwa 1 g; bei einem harten Aufprall schnellt sie auf mehrere g hoch. Die zweite ist die Neigung, also der Winkel zwischen Stock und Senkrechter. Liegt der X-Pfeil der Platine entlang des Schafts, gilt atan2(√(ay² + az²), |ax|): Aufrecht liest du 0 Grad, beim Gehen 40–55, am Boden liegend 85–90. Systeme zur Sturzerkennung bei Menschen suchen zuerst einen kurzen freien Fall (einen Moment nahe 0 g); ein umkippender Stock bewegt sich aber wie ein Stab, der sich um seine Spitze dreht, deshalb sieht ein Sensor am Griff diesen Moment oft gar nicht. Unser Algorithmus achtet daher nicht auf den freien Fall, sondern auf einen Aufprall mit anschließender flacher, regloser Lage.
| Phase | Bedingung | Dauer | Was passiert? |
|---|---|---|---|
| WALK (Gehen) | Gesamtbeschleunigung über 2,5 g | Unbegrenzt | Wechsel zu SETTLE; Hinderniswarnung läuft weiter |
| SETTLE (Beruhigen) | Warten, bis das Wackeln aufhört | 1 s | Wechsel zu CHECK |
| CHECK (Prüfen) | Neigung muss über 70° und Beschleunigung zwischen 0,8 und 1,2 g bleiben | 2 s | Bricht die Bedingung, zurück zu WALK, sonst ALARM |
| ALARM | Jede Sekunde Piepton und Vibration, damit man den Stock findet | 20 s | Abbruch per Tastendruck oder durch Aufheben des Stocks (Neigung unter 60°) |
| SOS | ... --- ... im Morsecode mit Ton, LED und Vibration | Bis zum Abbruch | Kurzer Druck beendet es; der ESP32 schickt eine SMS |
Diese Abfolge verhindert die meisten Fehlalarme. Auch das Tippen mit dem Stock beim Gehen kann kurze Stöße über 2,5 g erzeugen, aber eine Sekunde später ist der Stock noch in der Hand und 40–55 Grad geneigt, also wird die Prüfphase sofort abgebrochen; der Nutzer merkt davon nichts. Fällt der Stock dagegen und bleibt flach liegen, startet der Alarm nach etwa 3 Sekunden, und der sekündliche Piepton hilft, den heruntergefallenen Stock nach Gehör wiederzufinden. Hebt die Person ihn auf, verstummt der Alarm von selbst. Reagiert 20 Sekunden lang niemand, nimmt der Code einen möglichen echten Sturz an und startet SOS. Hältst du den Taster 2 Sekunden gedrückt, startest du jederzeit manuell ein SOS.
Test- und Kalibriercode
Schwellenwerte unterscheiden sich je nach Stock, Untergrund und Sensor ein wenig. Lade vor dem Hauptcode den folgenden Sketch hoch: Er schreibt alle Viertelsekunde den Abstand, den Rohwert des Wassersensors, die höchste Beschleunigung der letzten Viertelsekunde und die Neigung in den seriellen Monitor. Wenn du die Werte in einer Tabelle notierst, füllst du nebenbei auch den Ergebnisteil deines Projektberichts.
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}- Miss den Wassersensor trocken und mit der Spitze 1 cm tief in einem Glas Wasser. Liest du zum Beispiel trocken 0–20 und nass 520, wähle für WATER_LIMIT den Mittelwert von 270–300.
- Halte den Stock wie gewohnt und pendle ihn hin und her, als würdest du gehen. Notiere die Neigung; sie liegt meist zwischen 40 und 55 Grad und muss unter PICKUP_DEG (60) bleiben.
- Lege den Stock flach auf den Boden. Die Neigung sollte 85–90 und die Beschleunigung etwa 0,95–1,05 g betragen; das bestätigt LYING_DEG (70) und das Fenster von 0,8–1,2 g.
- Tippe beim Gehen wie gewohnt auf den Boden und beobachte die Spitzenwerte in g. Lass den Stock dann 10-mal auf eine Matte fallen. Stelle IMPACT_G unter die Sturzwerte; selbst wenn Tippstöße diese Schwelle überschreiten, filtert die Neigungsprüfung sie heraus.
- Nimm je 10 Messungen in 30, 60, 100 und 150 cm Abstand von einer Wand und vergleiche den Mittelwert mit dem Maßband.
Vollständiger Code: Hindernisse, Wasser, Sturz, SOS und Akkuwarnung
Im vollständigen Code laufen die vier Aufgaben in derselben Schleife, ohne sich gegenseitig zu blockieren, und die Sturzerkennung ist ein kleiner Zustandsautomat mit enum und switch-case. Der Taster hat drei Funktionen: Beim Gehen schaltet ein kurzer Druck die Hindernis-Pieptöne ein oder aus (die Vibration bleibt immer an), während eines Alarms oder SOS bricht ein kurzer Druck ab, und ein langer Druck von 2 Sekunden startet jederzeit ein SOS. Ein hoher Piepton bedeutet Ton an, ein tiefer Ton aus.
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}| Funktion oder Einstellung | Was macht sie? |
|---|---|
| pingCm() | Führt eine Messung aus; liefert 999, wenn innerhalb von 12 ms kein Echo kommt |
| median3() | Bildet den Median der letzten drei Messungen und verwirft einzelne Ausreißer |
| readMpu() | Liest die sechs Bytes Beschleunigungsdaten und berechnet Gesamtbeschleunigung und Neigung |
| fallUpdate() | Führt den Zustandsautomaten WALK → SETTLE → CHECK → ALARM → SOS aus |
| waterUpdate() | Versorgt den Wassersensor 10 ms lang, liest ihn und startet beim Wechsel von trocken zu nass eine Warnung |
| walkOutput() | Erzeugt die abstandsabhängige Vibration und die Wasserwarnung als Zweiklang |
| sosOutput() | Spielt das Array SOS_PATTERN Schritt für Schritt ab |
| readButton() | Unterscheidet kurzen (1) und langen (2) Druck und ignoriert Prellen unter 30 ms |
| IMPACT_G, LYING_DEG, CANCEL_MS | Sturzempfindlichkeit und Abbruchzeit; mit dem Test-Sketch einstellen |
Das SOS-Muster folgt den Regeln des Morsecodes: Ein Punkt dauert 1 Einheit, ein Strich 3 Einheiten, zwischen Zeichen liegt 1, zwischen Buchstaben 3 und zwischen Wiederholungen 7 Einheiten Pause. Mit 150 ms pro Einheit dauert ein SOS-Durchlauf 34 Einheiten, also etwa 5,1 Sekunden. Der 3-kHz-Ton ist bewusst gewählt: Das menschliche Ohr ist zwischen 2 und 4 kHz am empfindlichsten, und kleine passive Buzzer sind in diesem Bereich auch am lautesten. Den Unterschied zwischen aktivem und passivem Buzzer erklärt der Artikel Was ist ein Buzzer?.
Akku, Stromversorgung und Laufzeit
Ein smarter Stock ist den ganzen Tag draußen im Einsatz, daher ist die Stromversorgung genauso wichtig wie die Sensoren. Schätzen wir den Durchschnittsverbrauch: Der Nano zieht etwa 20 mA, der HC-SR04 15 mA und der MPU6050 4 mA. Der Vibrationsmotor braucht um die 70 mA, läuft aber nur während der Impulse; bei einem typischen Spaziergang sind es im Mittel etwa 10 mA. Buzzer und Wassersensor kommen mit ein paar mA dazu. Insgesamt sind das rund 50 mA, also 0,25 W bei 5 V. Eine 18650-Zelle mit 2600 mAh speichert bei 3,7 V etwa 9,6 Wh; über einen Aufwärtswandler mit 85 % Wirkungsgrad kommen rund 8 Wh bei der Schaltung an. 8 / 0,25 = 32 Stunden; da man den Akku nie ganz leeren kann, sind 25–30 Stunden realistisch. Wer den Stock zwei Stunden am Tag nutzt, lädt ihn also etwa alle zwei Wochen. Deine eigene Rechnung machst du mit dem Werkzeug Akkulaufzeit, und den Unterschied zwischen mAh und Wh erklärt der Artikel mAh und Wh.
| Stromquelle | Gewicht | Laufzeit ca. (50 mA) | Bewertung |
|---|---|---|---|
| 9-V-Blockbatterie (etwa 500 mAh), an VIN | 45 g | 8–10 Stunden | Nicht empfohlen: Spannung bricht ein, wenn der Motor läuft, nicht aufladbar |
| 4 × AA NiMH (2000 mAh), an den 5V-Pin | Etwa 120 g | 35–40 Stunden | Schwer; frisch geladen bis 5,6 V, also an der Grenze |
| 1 × 18650 + TP4056 + MT3608 | Etwa 55 g | 25–30 Stunden | Empfohlen: leicht, per USB ladbar, geschützt |
| 2 × 18650 in Reihe (7,4 V), an VIN | Etwa 105 g | 45–50 Stunden | Lange Laufzeit, aber schwer, braucht eigene Ladeschaltung |
| Powerbank (10.000 mAh) | 200 g und mehr | Mehrere Tage | Schwer; die meisten Powerbanks schalten bei geringem Strom ab |

Die empfohlene Stromversorgungskette sieht so aus: Die 18650-Zelle wird an B+ und B− des TP4056-Moduls angeschlossen. Die Leitung von OUT+ läuft über einen Schiebeschalter zum Eingang des MT3608-Aufwärtswandlers. Dessen auf 5,0 V eingestellter Ausgang versorgt den 5V-Pin des Nano, den HC-SR04 und das Vibrationsmodul. Zur Messung der Akkuspannung wird direkt hinter dem Schalter ein Teiler aus zwei 100-kΩ-Widerständen angeschlossen, dessen Mitte an A0 geht; weil er hinter dem Schalter sitzt, entlädt er den Akku im ausgeschalteten Zustand nicht. Der DW01-Chip trennt die Zelle bei etwa 2,4–2,5 V, doch der Code warnt viel früher: Unter 3,4 V spielt er alle 30 Sekunden einen tiefen Piepton. Details im Artikel TP4056-Lademodul.
Gehäuse, Gewicht und Ergonomie
Die meisten faltbaren Langstöcke wiegen 200–300 g, und die Nutzer pendeln sie stundenlang aus dem Handgelenk hin und her. Jedes zusätzliche Gramm wird mit seinem Abstand zur Hand multipliziert und belastet das Handgelenk. Ein 150-g-Gehäuse 10 cm unter dem Griff erzeugt ein zusätzliches Drehmoment von 150 × 10 = 1,5 kg·cm; dasselbe Gehäuse 60 cm tiefer käme auf 9 kg·cm, das Sechsfache. Pack deshalb die gesamte Elektronik in ein Gehäuse direkt unter dem Griff; nur der Wassersensor und sein dünnes Kabel laufen zur Spitze. Ziel: weniger als 150 g Zusatzgewicht.
| Bauteil | Gewicht ca. |
|---|---|
| Arduino Nano | 7 g |
| HC-SR04 | 9 g |
| MPU6050 (GY-521) | 2 g |
| Vibrationsmodul + passiver Buzzer | 5 g |
| Wasserstandssensor mit Kabel | 8 g |
| 18650-Zelle | 45–48 g |
| TP4056 + MT3608 + Schalter | 8 g |
| 3D-gedrucktes Gehäuse und Schellen | 40–50 g |
| Kabel, Schrauben, Klettband | 10–15 g |
| Gesamt | Etwa 135–150 g |
- Material: Drucke das Gehäuse aus PETG oder ABS; PLA kann in einem Auto in der Sonne weich werden und sich verziehen. 1,6–2 mm Wandstärke reichen aus.
- Befestigung: Befestige das Gehäuse mit zwei Schlauchschellen oder einem gummierten Klettband am Stock. Bei Faltstöcken muss das Gehäuse oberhalb des ersten Faltgelenks sitzen, sonst lässt sich der Stock nicht mehr falten.
- Sensorfenster: Die beiden Zylinder des HC-SR04 müssen vorne aus dem Gehäuse ragen; setze kein dünnes Plastik oder Gitter davor, Ultraschall kommt da nicht durch.
- Regen: Bohre unten ins Gehäuse ein kleines Ablaufloch und lege ein Tütchen Silikagel hinein. Regnet es bei dir oft, nimm statt des HC-SR04 den wasserdichten JSN-SR04T.
- Taster: Wähle einen großen Taster mit deutlichem Klick und setze ihn dorthin, wo der Daumen beim Halten von selbst liegt. Ein 2 mm hoher Rand drumherum verhindert versehentliches Drücken.
- Ein/Aus: Die Schalterstellung sollte sich ertasten lassen; der Einschaltpiepton bestätigt zusätzlich, dass das Gerät läuft.
- Ladebuchse: Die USB-Buchse des TP4056 sollte von der Unterseite des Gehäuses erreichbar sein und bei Nichtgebrauch mit einem Gummistopfen verschlossen werden.
- Sichtbarkeit: Reflektorband an den Gehäuseseiten hilft Autofahrern, den Stock abends beim Überqueren der Straße zu bemerken.
Standort senden mit ESP32 + GPS
Mit einem Arduino kann der Stock bei einem SOS nur Menschen in der Nähe akustisch alarmieren. Mit einem ESP32 und einem GPS-Modul lässt sich der Standort dagegen als Nachricht an Angehörige schicken. Das NEO-6M-Modul sendet standardmäßig mit 9600 Baud NMEA-Sätze mit einer Position pro Sekunde; die Bibliothek TinyGPSPlus wertet sie aus und liefert Breitengrad, Längengrad und Satellitenanzahl. Laut Datenblatt dauert der erste Fix nach einem Kaltstart etwa 27 Sekunden, in der Praxis draußen zwischen 30 Sekunden und einigen Minuten; in Innenräumen gibt es oft gar keinen Fix. Die kleine Pufferbatterie auf dem Modul speichert die letzten Satellitendaten, deshalb ist die Position bei späteren Starts in wenigen Sekunden da. Details im Artikel NEO-6M GPS-Modul.

Es gibt zwei Wege, die Nachricht zu verschicken. Für eine Vorführung in der Schule ist es am einfachsten, den ESP32 mit dem persönlichen Hotspot des Handys zu verbinden und die Nachricht über einen Telegram-Bot zu senden. Für einen Stock, der wirklich auf der Straße funktioniert, passt ein SIM800L-GSM-Modul besser, weil es nicht vom Handy abhängt: Es verschickt mit der eingesetzten SIM-Karte direkt eine SMS. Der SIM800L braucht 3,4–4,4 V und zieht beim Senden Stromspitzen bis 2 A; versorge ihn deshalb direkt aus der 18650-Zelle statt über den Aufwärtswandler und setze einen 1000-µF-Kondensator an die Versorgungspins. Das Modul funktioniert nur im 2G-Netz; in Ländern, in denen 2G abgeschaltet wurde, brauchst du ein 4G-Modul wie A7670 oder SIM7600. Wenn alles auf einer Platine sitzen soll, nimm ein TTGO T-Call (ESP32 + SIM800L).
1// ESP32 akıllı baston: engel uyarısı, su ve düşme algılama, SOS'ta GPS konumunu SMS ile gönderme
2// ESP32 smart cane: obstacle alert, water and fall detection, sends the GPS location by SMS on SOS
3// Kütüphane: TinyGPSPlus (Mikal Hart) / Library: TinyGPSPlus (Mikal Hart)
4#include <Wire.h>
5#include <TinyGPSPlus.h>
6
7// Pinler / Pins
8const int TRIG = 18, ECHO = 19; // ECHO 1 kΩ / 2 kΩ bölücüyle / ECHO via a 1 kΩ / 2 kΩ divider
9const int VIB = 25, BUZZER = 26, BUTTON = 27;
10const int WATER_PWR = 33, WATER_SIG = 34; // ADC1 pini / ADC1 pin
11const int BAT = 35; // 100k/100k bölücü / 100k/100k divider
12const int GPS_RX = 16, GPS_TX = 17; // NEO-6M TX → 16, RX ← 17
13const int GSM_RX = 13, GSM_TX = 23; // SIM800L TXD → 13, RXD ← 23
14const byte MPU = 0x68; // SDA 21, SCL 22
15
16const char* PHONE = "+905XXXXXXXXX"; // SMS gidecek numara / number to text
17
18const int NEAR_CM = 30, FAR_CM = 150, PULSE_MS = 60;
19const int WATER_LIMIT = 1200; // 12 bit ölçekte, test ederek bul / on the 12-bit scale, find by testing
20const float IMPACT_G = 2.5, LYING_DEG = 70, PICKUP_DEG = 60;
21const unsigned long CANCEL_MS = 20000, SMS_REPEAT_MS = 300000; // 5 dakikada bir konum / location every 5 minutes
22
23enum Mode { WALK, SETTLE, CHECK, ALARM, SOS };
24Mode mode = WALK;
25const int8_t SOS_PATTERN[] = {1, -1, 1, -1, 1, -3, 3, -1, 3, -1, 3, -3, 1, -1, 1, -1, 1, -7};
26const int SOS_LEN = sizeof(SOS_PATTERN) / sizeof(SOS_PATTERN[0]);
27const unsigned long UNIT_MS = 150;
28
29TinyGPSPlus gps;
30bool soundOn = true, wet = false, waterAlert = false, waterHigh = false, sosOn = false, smsDue = false;
31bool btnDown = false, longFired = false;
32int distanceCm = 999, vibNow = -1, sosStep = 0, histIdx = 0;
33int hist[3] = {999, 999, 999};
34float gTotal = 1.0, tilt = 45;
35unsigned long lastPing = 0, lastPulse = 0, lastMpu = 0, lastWater = 0, lastBat = 0, lastBeep = 0;
36unsigned long modeStart = 0, waterStart = 0, sosStepStart = 0, btnSince = 0, lastSms = 0;
37
38void setVib(int v) {
39 if (v != vibNow) { analogWrite(VIB, v); vibNow = v; }
40}
41
42void mpuWrite(byte reg, byte val) {
43 Wire.beginTransmission(MPU);
44 Wire.write(reg);
45 Wire.write(val);
46 Wire.endTransmission();
47}
48
49bool readMpu() {
50 Wire.beginTransmission(MPU);
51 Wire.write(0x3B);
52 if (Wire.endTransmission(false) != 0) return false;
53 if (Wire.requestFrom(MPU, (uint8_t)6) != 6) return false;
54 float a[3];
55 for (int i = 0; i < 3; i++) {
56 uint8_t hi = Wire.read();
57 uint8_t lo = Wire.read();
58 a[i] = (int16_t)(((uint16_t)hi << 8) | lo) / 4096.0; // ±8 g
59 }
60 gTotal = sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]);
61 tilt = atan2(sqrt(a[1] * a[1] + a[2] * a[2]), fabs(a[0])) * 180.0 / PI;
62 return true;
63}
64
65int pingCm() {
66 digitalWrite(TRIG, LOW); delayMicroseconds(2);
67 digitalWrite(TRIG, HIGH); delayMicroseconds(10);
68 digitalWrite(TRIG, LOW);
69 unsigned long t = pulseIn(ECHO, HIGH, 12000UL);
70 return t == 0 ? 999 : (int)(t / 58);
71}
72
73int median3(int a, int b, int c) {
74 if ((a <= b && b <= c) || (c <= b && b <= a)) return b;
75 if ((b <= a && a <= c) || (c <= a && a <= b)) return a;
76 return c;
77}
78
79int readButton(unsigned long now) { // 0 yok, 1 kısa, 2 uzun / 0 none, 1 short, 2 long
80 bool pressed = digitalRead(BUTTON) == LOW;
81 if (pressed && !btnDown) { btnDown = true; longFired = false; btnSince = now; }
82 if (pressed && btnDown && !longFired && now - btnSince >= 2000) { longFired = true; return 2; }
83 if (!pressed && btnDown) {
84 btnDown = false;
85 if (!longFired && now - btnSince >= 30) return 1;
86 }
87 return 0;
88}
89
90void feedGps() { // NMEA cümlelerini çöz / parse the NMEA sentences
91 while (Serial2.available()) gps.encode(Serial2.read());
92}
93
94// SIM800L'den belirli bir yanıtı bekle / wait for a given reply from the SIM800L
95bool waitFor(const char* token, unsigned long timeoutMs) {
96 String buf;
97 unsigned long start = millis();
98 while (millis() - start < timeoutMs) {
99 while (Serial1.available()) {
100 buf += (char)Serial1.read();
101 if (buf.indexOf(token) >= 0) return true;
102 if (buf.length() > 200) buf.remove(0, 100);
103 }
104 feedGps(); // beklerken GPS'i kaçırma / keep reading GPS while waiting
105 }
106 return false;
107}
108
109bool sendSms(const String& text) {
110 Serial1.println("AT"); // ilk AT baud hızını eşler / the first AT syncs the baud rate
111 if (!waitFor("OK", 1000)) {
112 Serial1.println("AT");
113 if (!waitFor("OK", 1000)) return false;
114 }
115 Serial1.println("AT+CMGF=1"); // metin modu / text mode
116 if (!waitFor("OK", 1000)) return false;
117 Serial1.print("AT+CMGS=\"");
118 Serial1.print(PHONE);
119 Serial1.println("\"");
120 if (!waitFor(">", 5000)) return false; // mesaj istemi / message prompt
121 Serial1.print(text);
122 Serial1.write(26); // Ctrl+Z: gönder / Ctrl+Z: send
123 return waitFor("+CMGS", 20000); // şebeke onayı / network confirmation
124}
125
126String sosText() { // SMS'te Türkçe harf kullanma / no Turkish letters in the SMS
127 String s = "SOS! Akilli baston yardim cagrisi. ";
128 if (gps.location.isValid()) {
129 s += "Konum: https://maps.google.com/?q=";
130 s += String(gps.location.lat(), 6) + "," + String(gps.location.lng(), 6);
131 s += " (" + String(gps.location.age() / 1000) + " sn once, " + String(gps.satellites.value()) + " uydu)";
132 } else {
133 s += "GPS konumu henuz yok.";
134 }
135 return s;
136}
137
138void setMode(Mode m, unsigned long now) {
139 mode = m;
140 modeStart = now;
141 sosStep = 0;
142 sosStepStart = now;
143 sosOn = false;
144 setVib(0);
145 noTone(BUZZER);
146 if (m == SOS) { smsDue = true; lastSms = now; }
147}
148
149void fallUpdate(unsigned long now) {
150 if (mode == WALK && gTotal > IMPACT_G) setMode(SETTLE, now);
151 else if (mode == SETTLE && now - modeStart >= 1000) setMode(CHECK, now);
152 else if (mode == CHECK) {
153 if (tilt < LYING_DEG || fabs(gTotal - 1.0) > 0.2) setMode(WALK, now);
154 else if (now - modeStart >= 2000) setMode(ALARM, now);
155 } else if (mode == ALARM) {
156 if (tilt < PICKUP_DEG) setMode(WALK, now);
157 else if (now - modeStart >= CANCEL_MS) setMode(SOS, now);
158 }
159}
160
161void waterUpdate(unsigned long now) {
162 digitalWrite(WATER_PWR, HIGH);
163 delay(10);
164 int v = analogRead(WATER_SIG); // 0–4095
165 digitalWrite(WATER_PWR, LOW);
166 if (v > WATER_LIMIT && !wet) { wet = true; waterAlert = true; waterStart = now; }
167 else if (v < WATER_LIMIT - 400) wet = false;
168}
169
170void walkOutput(unsigned long now) {
171 if (waterAlert && now - waterStart >= 1800) waterAlert = false;
172 if (waterAlert && now - lastBeep >= 300) {
173 lastBeep = now;
174 waterHigh = !waterHigh;
175 tone(BUZZER, waterHigh ? 1600 : 1000, 250);
176 }
177 if (distanceCm < 2 || distanceCm > FAR_CM) {
178 setVib(0);
179 } else if (distanceCm <= NEAR_CM) {
180 setVib(255);
181 if (soundOn && !waterAlert && now - lastBeep >= 120) { lastBeep = now; tone(BUZZER, 2500, 60); }
182 } else {
183 unsigned long period = map(distanceCm, NEAR_CM, FAR_CM, 120, 900);
184 int strength = map(distanceCm, NEAR_CM, FAR_CM, 255, 150);
185 if (now - lastPulse >= period) {
186 lastPulse = now;
187 if (soundOn && !waterAlert) tone(BUZZER, 2000, 30);
188 }
189 setVib(now - lastPulse < PULSE_MS ? strength : 0);
190 }
191}
192
193void sosOutput(unsigned long now) {
194 if (smsDue || now - lastSms >= SMS_REPEAT_MS) { // girişte ve 5 dakikada bir / on entry and every 5 minutes
195 smsDue = false;
196 lastSms = now;
197 noTone(BUZZER);
198 bool ok = sendSms(sosText());
199 Serial.println(ok ? "SMS gonderildi / SMS sent" : "SMS gonderilemedi / SMS failed");
200 sosStepStart = millis();
201 sosOn = false;
202 }
203 if (now - sosStepStart >= (unsigned long)abs(SOS_PATTERN[sosStep]) * UNIT_MS) {
204 sosStep = (sosStep + 1) % SOS_LEN;
205 sosStepStart = now;
206 }
207 bool on = SOS_PATTERN[sosStep] > 0;
208 if (on != sosOn) {
209 sosOn = on;
210 if (on) tone(BUZZER, 3000); else noTone(BUZZER);
211 setVib(on ? 255 : 0);
212 }
213}
214
215void setup() {
216 Serial.begin(115200);
217 Serial2.begin(9600, SERIAL_8N1, GPS_RX, GPS_TX); // NEO-6M varsayılanı 9600 baud / NEO-6M default is 9600 baud
218 Serial1.begin(9600, SERIAL_8N1, GSM_RX, GSM_TX); // SIM800L
219 pinMode(BUTTON, INPUT_PULLUP);
220 pinMode(TRIG, OUTPUT);
221 pinMode(ECHO, INPUT);
222 pinMode(WATER_PWR, OUTPUT);
223 Wire.begin(21, 22);
224 mpuWrite(0x6B, 0x00);
225 mpuWrite(0x1A, 0x03);
226 mpuWrite(0x1C, 0x10);
227 tone(BUZZER, 1500, 100);
228}
229
230void loop() {
231 unsigned long now = millis();
232 feedGps();
233
234 int b = readButton(now);
235 if (b == 2) setMode(SOS, now);
236 else if (b == 1) {
237 if (mode == ALARM || mode == SOS) setMode(WALK, now); // alarmı ya da SOS'u iptal et / cancel the alarm or SOS
238 else { soundOn = !soundOn; tone(BUZZER, soundOn ? 2000 : 600, 120); } // yürürken: ses aç/kapat / while walking: sound on/off
239 }
240
241 if (now - lastMpu >= 20) {
242 lastMpu = now;
243 if (readMpu()) fallUpdate(now);
244 }
245
246 if (mode == WALK || mode == SETTLE || mode == CHECK) {
247 if (now - lastPing >= 60) {
248 lastPing = now;
249 hist[histIdx] = pingCm();
250 histIdx = (histIdx + 1) % 3;
251 distanceCm = median3(hist[0], hist[1], hist[2]);
252 }
253 if (now - lastWater >= 500) { lastWater = now; waterUpdate(now); }
254 walkOutput(now);
255 } else if (mode == ALARM) {
256 setVib((now - modeStart) % 1000 < 150 ? 255 : 0);
257 if (now - lastBeep >= 1000) { lastBeep = now; tone(BUZZER, 2000, 150); }
258 } else {
259 sosOutput(now);
260 }
261
262 if (now - lastBat >= 30000) {
263 lastBat = now;
264 float v = analogReadMilliVolts(BAT) * 2 / 1000.0; // kalibreli mV okuma / calibrated mV reading
265 Serial.printf("Pil / Battery: %.2f V, GPS: %s\n", v, gps.location.isValid() ? "var / fix" : "yok / no fix");
266 if (v < 3.4 && mode == WALK) tone(BUZZER, 800, 400);
267 }
268}Der Code nutzt denselben Zustandsautomaten wie die Arduino-Version; der Unterschied liegt beim SOS. Sobald das SOS startet, schickt sendSms() dem SIM800L nacheinander AT, AT+CMGF=1 für den Textmodus und AT+CMGS="Nummer", wartet auf die Eingabeaufforderung ">", schreibt die Nachricht und versendet sie mit Strg+Z (26). Der Standort steht als Kartenlink in der Nachricht und öffnet auf dem Handy per Fingertipp die Karten-App. Hat das GPS noch keinen Fix, steht auch das in der Nachricht, und solange das SOS läuft, wird der Standort alle 5 Minuten erneut gesendet. Der SMS-Text verzichtet bewusst auf türkische Sonderzeichen: Buchstaben wie ş, ğ und ı gehören nicht zum Standard-GSM-Alphabet und erscheinen auf manchen Handys verstümmelt. Beim Senden kann der Code bis zu 30 Sekunden warten; nach dem Einschalten braucht der SIM800L außerdem 10–30 Sekunden, um sich ins Netz einzubuchen.
Smarter Blindenstock mit dem Raspberry Pi Pico (MicroPython)
Auf dem Raspberry Pi Pico wird dieselbe Logik in MicroPython umgesetzt. time_pulse_us() arbeitet wie pulseIn() beim Arduino und liefert einen negativen Wert, wenn kein Echo kommt. PWM-Ausgänge werden mit dem 16-Bit-Befehl duty_u16() gesetzt; beim Vibrationsmotor bedeutet 65535 volle Leistung. Die sechs Bytes des MPU6050 werden mit i2c.readfrom_mem() in einem Rutsch gelesen und in vorzeichenbehaftete Zahlen umgewandelt, indem man von Werten über 32767 die Zahl 65536 abzieht. Auch die Pins des Pico arbeiten mit 3,3 V, deshalb ist der ECHO-Teiler hier ebenfalls Pflicht. Im Akkubetrieb speist du den 5-V-Ausgang des MT3608 in den VSYS-Pin ein; der HC-SR04 hängt an derselben 5-V-Schiene. Mit dem WLAN-fähigen Pico W kannst du den Zustand des Stocks auch auf einer Webseite anzeigen.
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)Tipps für die Präsentation bei Jugend forscht oder auf der Wissenschaftsmesse
Der smarte Blindenstock ist auf Wissenschaftsmessen und bei Schulwettbewerben ein häufiges Projekt (weitere Ideen findest du in unserem Artikel Projektideen für Wissenschaftswettbewerbe). Was die Jury überzeugt, ist daher nicht, dass der Stock vibriert, sondern wie gut du das Problem beschreibst und wie ehrlich du deine Lösung misst. Die stärksten Projekte beginnen mit einem Gespräch mit einer blinden Person oder einem Blindenverband: Von ihnen zu hören, welche Hindernisse wirklich ein Problem sind, ob sie Vibration oder Ton bevorzugen und wie schwer der Stock höchstens sein darf, gibt deinem Projekt eine Richtung und eine starke Geschichte.
- Problem: Der weiße Stock erkennt keine Hindernisse oberhalb der Hüfte, und Pfützen bleiben unbemerkt. Erkläre das mit konkreten Beispielen aus deinen Gesprächen.
- Ziel: Einen Aufsatz entwickeln, der Hindernisse in Körper- und Kopfhöhe mindestens 1 Sekunde vorher per Vibration meldet, ohne Gewicht und Handhabung des Stocks stark zu verändern.
- Methode: Zeige die Sensorwahl (HC-SR04, JSN-SR04T, ToF) mit Begründung, den Montagewinkel mit Berechnung und die Vibrationszuordnung mit Formel.
- Ergebnisse: Präsentiere die Ergebnisse der folgenden Tests in Tabellen und Diagrammen und nenne auch die Fälle, in denen es nicht geklappt hat.
- Fazit und Ausblick: Sag klar, was das Projekt löst, wo seine Grenzen liegen (Glas, weiche Oberflächen, Regen, Erkennung des Stocksturzes statt des Personensturzes) und was der nächste Schritt ist.
| Test | Durchführung | Zu notierender Wert |
|---|---|---|
| Messgenauigkeit | Je 10 Messungen in 30, 60, 100 und 150 cm Abstand zur Wand | Mittlerer Fehler (cm) und größte Abweichung |
| Hindernisarten | Wand, Person, 5-cm-Pfosten, Busch, Glastür, Stoffvorhang | Wie oft jedes Hindernis bei 10 Versuchen erkannt wird |
| Reaktionszeit | Zeitlupenvideo mit dem Handy: Erscheinen des Hindernisses und erste Vibration | Verzögerung in Millisekunden |
| Wassererkennung | Spitze 0,5, 1, 2 und 3 cm tief ins Wasser tauchen | Ab welcher Tiefe gewarnt wird |
| Sturzerkennung | 20 Stürze und 100 Tippschläge auf den Boden | Anzahl richtiger und falscher Alarme |
| Akkulaufzeit | Vom vollen Akku bis zur Abschaltung laufen lassen | Dauer in Stunden und Endspannung |
| Gewicht und Balance | Mit der Küchenwaage wiegen, Abstand zum Griff messen | Zusatzgewicht (g) und Drehmoment (kg·cm) |
Ideen zur Weiterentwicklung
- Stufen- und Lochwarnung: Ein zweiter HC-SR04, der schräg nach unten vorne zeigt, sieht den Boden normalerweise in konstantem Abstand; wird der Messwert plötzlich länger, liegt vorne eine Stufe nach unten oder ein Loch.
- Sprachausgabe: Spiele mit einem DFPlayer Mini statt Pieptönen aufgenommene Sätze wie "Hindernis vorne", "Wasser" oder "Akku schwach" über einen Ohrhörer ab.
- Handy-App: Zeige mit Bluetooth BLE des ESP32 Akkustand und Einstellungen auf dem Handy an und ändere die Reichweite von dort aus.
- Richtung: Setze links und rechts am Griff je einen Motor ein und zeige per Vibration, auf welcher Seite das Hindernis liegt.
- Schlafmodus: Schalte die Sensoren ab, wenn der MPU6050 merkt, dass sich der Stock seit Minuten nicht bewegt; mit ESP32 Deep Sleep vervielfacht sich die Akkulaufzeit.
- Sichtbarkeit bei Nacht: Lass über einen LDR bei Dunkelheit eine LED an der Stockspitze blinken, damit Autofahrer die Person schon von Weitem sehen.
Häufige Fehler und Lösungen
| Symptom | Mögliche Ursache | Lösung |
|---|---|---|
| Der Stock vibriert ununterbrochen | Der Sensor zeigt auf den Boden oder auf den Stock selbst | Montagewinkel erhöhen, den Bereich vor dem Sensor frei halten |
| Ab und zu Vibration ohne Grund | Einzelne Fehlechos oder eine andere Ultraschallquelle in der Nähe | Der Medianfilter ist schon drin; notfalls den Median aus 5 Messungen bilden |
| Bei großer Entfernung läuft der Motor nicht an | Der PWM-Wert liegt unter der Anlaufschwelle des Motors | Mindeststärke über 150 halten, den Motor mit Gummi befestigen |
| Der Arduino startet neu, wenn der Motor läuft | Der Motorstrom lässt die Versorgung einbrechen oder die Diode fehlt | Fertiges Modul verwenden, 100-µF-Kondensator an die Versorgung |
| Der MPU6050 liest immer 0 oder einen festen Wert | SDA und SCL vertauscht, Adresse 0x69 oder Chip schläft | Adresse mit dem I2C-Scanner suchen, prüfen, ob 0 in Register 0x6B geschrieben wird |
| Fehlalarme durch Tippen auf den Boden | IMPACT_G zu niedrig und Neigungsschwelle nahe am Gehwinkel | Neigung mit dem Test-Sketch messen, LYING_DEG auf 75 erhöhen |
| Die Wasserwarnung hört nie auf oder kommt nie | Falsche Schwelle oder oxidierte Leiterbahnen | Trocken- und Nasswerte neu messen, Sensor bei Bedarf tauschen |
| Der Buzzer ist stumm oder spielt immer denselben Ton | Ein aktiver Buzzer ist eingebaut | Für tone() einen passiven Buzzer verwenden |
| Die SMS wird nicht verschickt | SIM800L nicht im Netz eingebucht, schwache Versorgung oder PIN der SIM aktiv | Prüfen, ob die Modul-LED alle 3 Sekunden blinkt, PIN deaktivieren, Modul aus der 18650 versorgen |
Ein smarter Blindenstock vereint Abstandsmessung, haptisches Feedback, Ereigniserkennung mit einem Bewegungssensor und Stromversorgungsdesign in einem einzigen Projekt, und weil er ein echtes Bedürfnis anspricht, ist er eines der sinnvollsten Arduino-Projekte überhaupt. Beginne mit dem Grundcode aus dieser Anleitung, fülle deine Testtabellen aus und ergänze dann Schicht für Schicht Wasser-, Sturz- und GPS-Funktionen. In der BlueGrays-App findest du Projekte zur Abstandsmessung mit dem HC-SR04, einen Parksensor, Tonerzeugung mit dem Buzzer und Benachrichtigungen mit dem ESP32 Schritt für Schritt mit Schaltplänen und fertigem Code, sodass du jeden Teil deines Stocks zuerst einzeln ausprobieren kannst.
Häufige Fragen
Wie funktioniert ein smarter Blindenstock?
Ein HC-SR04-Ultraschallsensor nahe am Griff sendet Schallwellen aus und berechnet aus der Laufzeit des Echos den Abstand zum Hindernis. Abhängig von diesem Abstand steuert der Arduino einen Vibrationsmotor im Griff: Je näher das Hindernis, desto schneller und stärker vibriert er. In erweiterten Versionen erkennt ein Wassersensor an der Spitze Pfützen und ein MPU6050, dass der Stock umgefallen ist.
Welche Bauteile brauche ich für einen Arduino-Blindenstock?
Für die Grundversion genügen ein Arduino Nano, ein HC-SR04-Ultraschallsensor, ein Vibrationsmotor-Modul, ein passiver Buzzer und Steckbrücken. Die Vollversion ergänzt Wasserstandssensor, MPU6050, einen Taster sowie eine 18650-Zelle mit TP4056-Lademodul und MT3608-Aufwärtswandler. Für den Standortversand brauchst du einen ESP32, ein NEO-6M-GPS und ein SIM800L-GSM-Modul.
Wie weit erkennt ein smarter Blindenstock Hindernisse?
Der HC-SR04 misst von 2 bis 400 cm, am Stock wird aber meist eine Grenze von 150 cm verwendet. Wer etwa 1 Meter pro Sekunde geht, bekommt so 1,5 Sekunden Vorwarnung, bevor er das Hindernis erreicht. Eine viel größere Reichweite führt an belebten Orten zu Dauervibration und dazu, dass Warnungen ignoriert werden.
Wie erkennt ein smarter Stock, dass er umgefallen ist?
Der Beschleunigungssensor MPU6050 wird 50-mal pro Sekunde gelesen. Bleibt der Stock nach einem Stoß über 2,5 g zwei Sekunden lang flach und reglos liegen, gilt er als umgefallen und ein Suchton startet. Wird er nicht innerhalb von 20 Sekunden aufgehoben oder der Taster gedrückt, ertönt SOS; die ESP32-Version verschickt in diesem Moment den Standort per SMS.
Wie lange läuft ein Arduino-Blindenstock mit Akku?
Die Schaltung zieht im Mittel etwa 50 mA. Eine einzelne 18650-Zelle mit 2600 mAh, TP4056-Lademodul und MT3608-Aufwärtswandler reicht für etwa 25–30 Stunden, bei zwei Stunden Nutzung pro Tag also rund zwei Wochen. Eine 9-V-Blockbatterie hält nur 8–10 Stunden und wird nicht empfohlen, weil ihre Spannung einbricht, sobald der Motor läuft.
Kann man einem smarten Blindenstock GPS-Ortung hinzufügen?
Ja. Nimmst du statt eines Arduino einen ESP32 und ergänzt ein NEO-6M-GPS-Modul, kannst du Breiten- und Längengrad des Stocks auslesen. Mit einem SIM800L-GSM-Modul geht der Standort bei einem SOS als Kartenlink per SMS an Angehörige; mit WLAN funktioniert auch ein Telegram-Bot. GPS arbeitet im Freien, in Gebäuden findet es meist keine Position.
Projekte, die du danach bauen kannst
Alle Projekte ansehen
HC-SR04 Abstandsmessung (serielle Anzeige)
Der Sensor HC-SR04 funktioniert wie eine Fledermaus, die im Dunkeln ihren Weg findet: Er sendet einen Ton, den du nicht hörst, lauscht auf das Echo und misst, wie weit der Gegenstand vor ihm entfernt

Verwendung des Summers
Hast du dich mal gefragt, wie eine Türklingel oder ein Alarm seinen Ton erzeugt? Der Arduino lässt einen kleinen Summer in kurzen Abständen piepen: piep, Pause, piep, Pause. Du siehst, wie Strom zu Kl

Frequenzeinstellung mit Buzzer
Mit vier Knöpfen steuerst du den Ton eines Summers: Einer startet den Ton, einer macht ihn stumm, die anderen beiden machen ihn höher oder tiefer. Hältst du den Plus- oder Minusknopf gedrückt, ändert

Parksensor mit HC-SR04
Eine Mini-Ausgabe der Einparkhilfe im Auto: Je näher etwas kommt, desto schneller piept es. Der Abstandssensor misst das Hindernis vor ihm; ist es weiter als 20 Zentimeter entfernt, leuchtet die grüne

ESP32 HC-SR04 Ultraschall-Entfernungsmesser
Der Ultraschallsensor HC-SR04 berechnet die Entfernung, indem er die Zeit misst, die die von ihm ausgesendete Schallwelle benötigt, um auf ein Objekt zu treffen und zurückzukehren (Time of Flight). In diesem Projekt senden wir mit dem ESP32 einen 10-Mikrosekunden-Impuls an den TRIG-Pin und lesen die vom ECHO-Pin kommende Echozeit mit pulseIn(). Die berechnete Entfernung geben wir in cm auf einem 16x2-I2C-LCD aus. Da der HC-SR04 mit 5 V betrieben wird, ist ein Spannungsteiler am ECHO-Pin zw

ESP32-Parksensor (HC-SR04 + 5-LED-Leiste + Summer + OLED)
Die ESP32-Version des Parkhilfesensors für Autos! Mit dem Ultraschallsensor HC-SR04 messen wir den Abstand zum Objekt vor dem Fahrzeug. Je weiter sich das Objekt nähert, desto mehr leuchten die 5 LEDs der Leiste auf (grün → gelb → rot) und die Intervalle der Summer-Signaltöne werden kürzer. Wenn das Objekt sehr nahe kommt (unter 10 cm), ertönt ein Daueralarm und alle LEDs leuchten. Auf dem SSD1306-OLED-Display werden der aktuelle Wert in cm in großer Schrift, ein Statustext (NICHT / VORSICHT / N

Roboter zur Hindernisvermeidung
Wie wäre es, ein intelligentes Fahrzeug zu bauen, das seinen Weg selbst findet? Der Obstacle Avoiding Robot scannt dank des Ultraschallsensors vor ihm ständig seine Umgebung. Wenn er auf ein Hindernis

ESP32 Bewegungsalarmsystem (PIR + Buzzer + LED + Telegram)
Ein intelligentes Sicherheitsalarmsystem, das Buzzer und LED aktiviert, sobald der PIR-Sensor Bewegung erkennt, und gleichzeitig eine sofortige Benachrichtigung per Telegram an den Besitzer sendet. Ein 30-sekündiger Cooldown begrenzt die Häufigkeit wiederholter Alarme; dieser Mechanismus überprüft weder die Gültigkeit der PIR-Erkennungen noch verhindert er Fehlauslösungen. Geeignet für Heimsicherheit, Garagenüberwachung, Lagerräume oder Arbeitszimmer. Mit ESP32-CAM sind Foto-Benachrichtigungen m
Weitere Artikel
BlueGrays-App
Diese Schaltung Schritt für Schritt auf dem Handy nachbauen
Schaltplan, Code und Teileliste auf einem Bildschirm. Über 200 Arduino-, ESP32- und Pico-Projekte zum Nachbauen.
Mit dem Handy scannen
Richte die Kamera auf den Code, dann öffnet sich der App-Store deines Handys.
oder im Store öffnen

