Bastone intelligente Arduino per non vedenti: ostacoli e cadute
Come costruire un bastone intelligente per ciechi con Arduino: ostacoli con HC-SR04, avviso a vibrazione, sensore d'acqua, cadute con MPU6050, SOS e codice.
Un bastone intelligente con Arduino avvisa una persona cieca o ipovedente degli ostacoli che il bastone bianco non riesce a rilevare, ed è uno dei progetti di elettronica più amati per i lavori scolastici e le fiere della scienza. Un sensore a ultrasuoni HC-SR04 montato vicino all'impugnatura misura la distanza da ciò che c'è davanti; man mano che l'ostacolo si avvicina, un motorino vibrante nell'impugnatura pulsa più in fretta e più forte e, se vuoi, un buzzer aggiunge un breve bip a ogni impulso. Così l'utente percepisce con qualche passo di anticipo gli ostacoli sopra la vita, che la punta del bastone non tocca mai.
In questa guida parti dal circuito di base e aggiungi, passo dopo passo, le estensioni che rendono il progetto davvero utilizzabile: un sensore di pozzanghere vicino alla punta del bastone, un algoritmo che usa un MPU6050 per capire che il bastone è caduto e lancia un SOS se nessuno lo annulla entro 20 secondi, un'alimentazione che dura più di 25 ore con una sola cella 18650, un contenitore progettato pensando a peso ed ergonomia e un'estensione ESP32 + GPS che invia la posizione via SMS. Alla fine troverai anche consigli concreti per collaudare il progetto e presentarlo a una fiera della scienza.

Che cos'è un bastone intelligente e a cosa serve?
Il bastone bianco è lo strumento di cui una persona cieca si fida di più: a ogni tocco rivela il suolo, il bordo del marciapiede, un gradino o un ostacolo basso. Ma un bastone "vede" solo ciò che tocca la sua punta. Gli ostacoli sopra la vita, come un ramo basso, un cartello che sporge sul marciapiede, lo specchietto di un camion, una finestra aperta o un condizionatore sulla facciata, non toccano mai la punta e colpiscono il corpo o la testa. Il bastone intelligente nasce per colmare questa lacuna: un sensore elettronico esplora le altezze e le distanze che il bastone non raggiunge e trasmette il risultato alla mano sotto forma di vibrazione.
Chiariamo subito una cosa: un bastone intelligente non sostituisce il bastone bianco né l'addestramento all'orientamento e alla mobilità, li completa. Il sensore a ultrasuoni ha dei punti ciechi e le batterie si scaricano; per questo la regola d'oro del progetto è che il bastone deve funzionare esattamente come prima anche con l'elettronica completamente spenta. La Giornata internazionale del bastone bianco, il 15 ottobre, è un'ottima occasione per presentare questo progetto a scuola.
Come funziona un bastone intelligente?
Il progetto è un unico ciclo che gira centinaia di volte al secondo e gestisce quattro compiti indipendenti contemporaneamente. Ogni 60 millisecondi Arduino misura la distanza, prende la mediana delle ultime tre letture e regola di conseguenza il ritmo della vibrazione. Ogni mezzo secondo legge il sensore d'acqua sulla punta. Cinquanta volte al secondo legge le accelerazioni dell'MPU6050 per controllare se il bastone è caduto. E ogni trenta secondi controlla la tensione della batteria. Il progetto ha due canali di uscita con compiti separati: la vibrazione serve solo per gli ostacoli, mentre il suono segnala situazioni particolari come acqua, caduta, SOS e batteria scarica. Se vuoi, una pressione breve del pulsante attiva o disattiva i bip degli ostacoli.
| Componente | Funzione | Nota |
|---|---|---|
| Arduino Nano (Uno per il prototipo) | Misura, decide, comanda motore e buzzer | Il Nano è adatto al bastone perché è piccolo e leggero |
| Sensore a ultrasuoni HC-SR04 | Misura la distanza dall'ostacolo davanti | 5 V, circa 15 mA; JSN-SR04T per la pioggia |
| Modulo motorino vibrante | Segnala l'ostacolo alla mano con la vibrazione | Modulo pronto con transistor e diodo |
| Buzzer passivo | Avvisi di acqua, caduta, SOS e batteria | Passivo per poter suonare toni diversi |
| Sensore di livello dell'acqua | Avvisa quando la punta entra in acqua | 1–2 cm sopra la punta, alimentato solo durante la misura |
| MPU6050 (GY-521) | Capisce che il bastone è caduto ed è a terra | I2C, indirizzo 0x68 |
| Pulsante | Suono sì/no, annulla allarme, SOS con pressione lunga | Posizionato sotto il pollice |
| Cella 18650 + TP4056 + MT3608 | Alimentazione portatile, ricarica USB, innalzamento a 5 V | Usa un TP4056 protetto (con DW01) |
| 2 resistenze da 100 kΩ | Partitore per misurare la tensione della batteria | Il punto centrale va ad A0 |
| Interruttore e contenitore | Accensione/spegnimento e protezione | Stampato in 3D o una piccola scatola per progetti |




Rilevamento ostacoli: come misura l'HC-SR04 e dove si monta?
Quando l'HC-SR04 riceve un impulso di 10 microsecondi su TRIG, emette otto onde sonore a 40 kHz e tiene ECHO a HIGH finché l'eco non torna. A 20 °C il suono percorre circa 343 metri al secondo; contando andata e ritorno, passano circa 58 microsecondi per ogni centimetro. La distanza si ottiene quindi semplicemente con cm = tempo (µs) / 58. Il sensore vede dentro un cono di circa 15 gradi; a 1,5 metri questo cono è largo circa 40 cm. Poiché il sensore ruota insieme al bastone quando lo muovi da un lato all'altro, esplora automaticamente una fascia larga circa un metro davanti a te. Trovi i dettagli del sensore nell'articolo Sensore di distanza HC-SR04.
Il dettaglio più importante del montaggio è l'angolo. Camminando, il bastone non è tenuto verticale ma inclinato in avanti di circa 40–50 gradi rispetto alla verticale. Se incolli il sensore parallelo al bastone, punta verso il suolo e lo scambia continuamente per un ostacolo. Fissa il contenitore vicino all'impugnatura, a circa 75 cm da terra, e orienta il sensore un po' sopra l'orizzontale. Con il bastone inclinato di 45 gradi, l'asse del sensore deve formare un angolo di 60 gradi con l'asta (misurato verso la punta) perché il sensore punti 15 gradi sopra l'orizzontale. Il modo più semplice per trovare l'angolo giusto è tenere il bastone come al solito e appoggiare un'app livella del telefono sul lato anteriore del contenitore.
| Angolo del sensore (sopra l'orizzontale) | Altezza esplorata a 1,5 m | Ostacoli tipici rilevati |
|---|---|---|
| 0° | Circa 55–95 cm | Panchine, bidoni, la carrozzeria di un'auto parcheggiata |
| 15° | Circa 95–135 cm | Il busto di una persona, il manubrio di una bici, il cassone di un camion |
| 25° | Circa 120–170 cm | Cartelli, rami, ante di finestre aperte, specchietti |
Se parti con un solo sensore, 15–20 gradi sono un buon compromesso: copre bene gli ostacoli all'altezza del corpo. Gli ostacoli all'altezza della testa, invece, entrano solo nel bordo superiore del cono finché sono lontani ed escono quando ti avvicini. Se l'altezza della testa è importante per te, dedicale un secondo HC-SR04 inclinato di 30–35 gradi verso l'alto; poiché questo sensore misura in obliquo, tieni la sua portata intorno ai 200 cm. Ricordati di attivare i due sensori uno dopo l'altro con almeno 60 ms di distanza, altrimenti uno sente l'eco dell'altro. Per un confronto dettagliato con i sensori laser (ToF) e a infrarossi, leggi l'articolo confronto tra sensori di distanza.
| Sensore | Portata | Pregi | Difetti |
|---|---|---|---|
| HC-SR04 | 2–400 cm | Economico, semplice, cono ampio | Non impermeabile, fatica con le superfici morbide |
| JSN-SR04T | Circa 25–450 cm | Sonda impermeabile, funziona sotto la pioggia | Non vede sotto i 25 cm, più costoso |
| VL53L1X (laser ToF) | Fino a 4 m | Piccolo, leggero, fascio preciso | Portata ridotta alla luce del sole, I2C a 3,3 V |
| Sharp GP2Y0A02 (infrarossi) | 20–150 cm | Uscita analogica, codice semplice | Si inganna con superfici scure e lucide, assorbe circa 33 mA |
Ritmo di vibrazione e bip in base alla distanza
Una persona cieca percepisce l'ambiente in gran parte con l'udito: il rumore di un'auto che si avvicina, il brusio della folla, l'eco dei colpi del bastone su un muro la guidano. Un buzzer che suona di continuo copre questi suoni invece di aiutare. Per questo il canale di avviso principale del progetto è la vibrazione, e la regola è semplice: più l'ostacolo è vicino, più gli impulsi di vibrazione sono frequenti e forti. Tutto ciò che è oltre 150 cm viene ignorato, e se un ostacolo è a meno di 30 cm il motore vibra senza sosta. Nel mezzo, l'intervallo tra gli impulsi si calcola in modo lineare con map(): intervallo = 120 + (distanza − 30) × 6,5 ms.
Perché 150 cm? Chi cammina con il bastone bianco avanza di circa 1 metro al secondo. Una portata di 150 cm gli lascia circa 1,5 secondi prima di raggiungere l'ostacolo; poiché il tempo di reazione a un avviso tattile è di 0,2–0,3 secondi, basta per fermarsi o cambiare direzione. Se porti la portata a 250 cm, su un marciapiede affollato il bastone vibra in continuazione e dopo un po' l'utente comincia a ignorare gli avvisi. In interni funzionano bene 100 cm, all'aperto 150–200 cm; basta cambiare la costante FAR_CM.
| Distanza | Intervallo tra impulsi | Forza della vibrazione (PWM) | Significato per l'utente |
|---|---|---|---|
| Oltre 150 cm | Nessuna vibrazione | 0 | Strada libera |
| 150 cm | 900 ms | 150 | C'è un ostacolo più avanti |
| 100 cm | 575 ms | 194 | L'ostacolo si avvicina, attenzione |
| 60 cm | 315 ms | 229 | Rallenta, preparati a cambiare direzione |
| 40 cm | 185 ms | 247 | Molto vicino |
| 30 cm o meno | Continua | 255 | Fermati |

Il motore più usato nei bastoni è un motore ERM a moneta (coin), come quello dei cellulari. Funziona a circa 3 V e assorbe 60–80 mA. Un pin di Arduino però fornisce al massimo 40 mA, con 20 mA come limite sicuro, quindi il motore non si collega direttamente al pin ma tramite un transistor. I moduli pronti contengono già questo transistor e il diodo che assorbe la tensione inversa quando il motore si ferma; la maggior parte funziona a 3,3–5 V. Con un motore nudo bastano un transistor NPN come il BC337 o il 2N2222, una resistenza di base da 1 kΩ e un diodo 1N4148 in antiparallelo al motore; puoi confrontare i transistor nello strumento Transistor.
La posizione del motore conta quanto il codice. Mettilo dentro l'impugnatura, dove poggiano il palmo o l'indice, e fissalo con gomma sottile invece che con la spugna, perché la spugna assorbe la vibrazione. Alla massima velocità un motore a moneta vibra a circa 200 Hz, e la nostra pelle è più sensibile alle vibrazioni intorno a 200–300 Hz, quindi anche un motorino piccolo nel punto giusto si sente bene. Poiché i motori ERM non partono con valori PWM molto bassi, la vibrazione più debole del codice è tenuta a 150 su 255. Nell'articolo motorino vibrante trovi di più sulla scelta del motore e sulla regolazione dell'intensità con il PWM.
Schema dei collegamenti: Arduino Uno/Nano, ESP32 e Raspberry Pi Pico
La tabella seguente corrisponde esattamente a tutti i codici di questo articolo. Sull'Uno e sul Nano i numeri dei pin sono gli stessi; su entrambe le schede il bus I2C dell'MPU6050 è su A4 (SDA) e A5 (SCL). Il VCC del modulo vibrante va a 5V, e il suo pin IN è stato messo apposta su D5: tone() usa il Timer2, quindi mentre suona il buzzer il PWM su D3 e D11 smette di funzionare, mentre D5 appartiene al Timer0 e non viene toccato. Puoi vedere la piedinatura del Nano nello strumento Pinout.
| Collegamento | Arduino Uno / Nano | ESP32 | Raspberry Pi Pico |
|---|---|---|---|
| HC-SR04 VCC | 5V | VIN (5 V) | VBUS o linea a 5 V |
| HC-SR04 TRIG | D9 | GPIO 18 | GP2 |
| HC-SR04 ECHO | D10 (diretto) | GPIO 19, con partitore 1 kΩ / 2 kΩ | GP3, con partitore 1 kΩ / 2 kΩ |
| Modulo vibrante IN | D5 (PWM) | GPIO 25 | GP15 |
| Buzzer passivo (+) | D8 | GPIO 26 | GP16 |
| Pulsante (altro piedino a GND) | D2 | GPIO 27 | GP14 |
| Sensore d'acqua + | D7 | GPIO 33 | GP22 |
| Sensore d'acqua S | A1 | GPIO 34 (ADC1) | GP26 (ADC0) |
| MPU6050 SDA | A4 | GPIO 21 | GP4 |
| MPU6050 SCL | A5 | GPIO 22 | GP5 |
| MPU6050 VCC | 5V | 3V3 | 3V3 |
| Misura batteria (centro del partitore 100k/100k) | A0 | GPIO 35 (ADC1) | GP28 (ADC2) |
| GPS NEO-6M TX / RX | Nessuno | GPIO 16 / GPIO 17 | Nessuno |
| SIM800L TXD / RXD | Nessuno | GPIO 13 / GPIO 23 | Nessuno |
Montaggio passo passo: prima il bastone di base
- Monta prima il circuito su una breadboard e lascia il fissaggio sul bastone per ultimo. Inserisci l'Arduino Nano al centro della breadboard e porta 5V e GND sulle linee laterali.
- Collega VCC e GND dell'HC-SR04 alle linee laterali, TRIG a D9 ed ECHO a D10.
- Collega il VCC del modulo vibrante a 5V, il suo GND a GND e il pin IN (S su alcuni moduli) a D5. Inserisci il piedino positivo del buzzer passivo su D8 e l'altro su GND.
- Carica il codice di base qui sotto e apri il monitor seriale a 9600 baud. Avvicina e allontana lentamente la mano dal sensore; la distanza sullo schermo deve cambiare e la vibrazione accelerare e rallentare di conseguenza.
- Tieni il sensore a 150, 100, 60 e 30 cm da un muro e confronta il valore del monitor seriale con un metro. Se la differenza supera 1–2 cm, controlla i collegamenti e l'alimentazione.
- Quando tutto funziona, aggiungi il sensore d'acqua, l'MPU6050 e il pulsante e passa al codice completo.
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}Nel codice non c'è nessun delay() lungo; ogni compito si basa su istanti di tempo registrati con millis(), quindi gli impulsi di vibrazione non rallentano mai le misure. Per pulseIn() è stato scelto un timeout di 12 ms: poiché 12.000 / 58 ≈ 200 cm, gli echi più lontani non ci servono comunque e un timeout breve evita che il ciclo si blocchi. Se non torna nessun eco, la funzione restituisce 999, che vale come "strada libera". setVib() scrive sul motore solo quando il valore cambia; dato che nelle versioni 2.x del core ESP32 analogWrite() riconfigura l'unità PWM a ogni chiamata, questo piccolo dettaglio fa funzionare senza problemi lo stesso codice anche lì.
Sensore di pozzanghere: avviso quando la punta entra in acqua
Le pozzanghere sono tra gli ostacoli più difficili da notare con il bastone bianco: quando la punta entra in acqua, il suono e la sensazione sono quasi uguali a quelli del suolo, eppure pochi centimetri d'acqua bastano per bagnarsi le scarpe. Per questo si usa un economico sensore di livello dell'acqua con piste di rame intrecciate. Alcune piste sono collegate all'alimentazione e altre alla linea del segnale; quando l'acqua le collega, la tensione del segnale sale e il valore di analogRead() aumenta. Asciutto, il sensore legge quasi 0, mentre con la punta in acqua sull'Uno dà di solito qualche centinaio; più va in profondità, più il valore cresce.
Monta il sensore nella parte più bassa del bastone, con il bordo inferiore circa 1–2 cm sopra la punta e le piste perpendicolari al suolo. Così sul terreno asciutto niente lo tocca, mentre in una pozzanghera più profonda di 1–2 cm le piste si bagnano. Il difetto noto di questi sensori è che le piste di rame si corrodono in fretta per elettrolisi se sono sempre alimentate. La soluzione del codice è collegare il pin + del sensore a D7 invece che direttamente a 5V e alimentarlo solo per i 10 ms della misura. Con una lettura ogni mezzo secondo, il sensore resta sotto tensione solo il 2% del tempo. Trovi il funzionamento nell'articolo sensore di livello dell'acqua e la differenza con la scheda pioggia nell'articolo sensore di pioggia.

Rilevamento cadute con l'MPU6050
L'MPU6050 è un sensore di movimento che riunisce su un solo chip un accelerometro a tre assi e un giroscopio a tre assi, collegato via I2C. La scheda GY-521 ha un regolatore, quindi può essere alimentata a 5 V; l'indirizzo predefinito è 0x68. In questo progetto usiamo solo l'accelerometro e parliamo direttamente con quattro registri invece di usare una libreria: scrivere 0 in 0x6B risveglia il chip, 0x1A attiva il filtro interno a 44 Hz, scrivere 0x10 in 0x1C porta la portata a ±8 g, e leggiamo le accelerazioni X, Y e Z dai sei byte che partono da 0x3B. La portata predefinita di ±2 g va facilmente in saturazione nel momento in cui il bastone colpisce il suolo e nasconde la vera entità dell'urto. Con la portata ±8 g, ogni 1 g corrisponde a 4096 conteggi grezzi. Per conoscere meglio il sensore, dai un'occhiata all'articolo accelerometro e giroscopio MPU6050.

A ogni lettura il codice calcola due grandezze. La prima è l'accelerazione totale: g = √(ax² + ay² + az²). Con il bastone fermo in mano o a terra vale circa 1 g; in un urto forte sale a diversi g. La seconda è l'inclinazione, cioè l'angolo tra il bastone e la verticale. Se monti la scheda con la freccia X lungo l'asta, l'inclinazione è atan2(√(ay² + az²), |ax|): segna 0 gradi con il bastone verticale, 40–55 camminando e 85–90 a terra. I sistemi che rilevano le cadute delle persone cercano prima una breve caduta libera (un istante vicino a 0 g); ma un bastone che si ribalta si muove come un'asta che ruota intorno alla sua punta, quindi un sensore vicino all'impugnatura spesso non vede mai quell'istante. Per questo il nostro algoritmo non cerca la caduta libera, ma un urto seguito da una posizione orizzontale e immobile.
| Fase | Condizione | Durata | Cosa succede? |
|---|---|---|---|
| WALK (cammino) | L'accelerazione totale supera 2,5 g | Illimitata | Passa a SETTLE; gli avvisi degli ostacoli continuano |
| SETTLE (assestamento) | Attende che finiscano le scosse | 1 s | Passa a CHECK |
| CHECK (verifica) | L'inclinazione deve restare sopra 70° e l'accelerazione tra 0,8 e 1,2 g | 2 s | Se la condizione salta torna a WALK, se regge parte ALARM |
| ALARM | Un bip e una vibrazione ogni secondo per ritrovare il bastone | 20 s | Si annulla con il pulsante o sollevando il bastone (inclinazione sotto 60°) |
| SOS | ... --- ... in codice Morse con suono, LED e vibrazione | Fino all'annullamento | Una pressione breve lo ferma; l'ESP32 invia un SMS |
Questa sequenza evita la maggior parte dei falsi allarmi. Anche battere il bastone a terra camminando può produrre brevi urti sopra 2,5 g, ma un secondo dopo il bastone è ancora in mano e inclinato di 40–55 gradi, quindi la fase di verifica si annulla subito; l'utente non se ne accorge nemmeno. Se invece il bastone cade e resta a terra, l'allarme parte dopo circa 3 secondi, e il bip ogni secondo permette a chi l'ha fatto cadere di ritrovarlo a orecchio. Se l'utente raccoglie il bastone, l'allarme si zittisce da solo. Se nessuno reagisce per 20 secondi, il codice presume che possa trattarsi di una vera caduta e avvia l'SOS. Tenere premuto il pulsante per 2 secondi avvia inoltre un SOS manuale in qualsiasi momento.
Codice di prova e calibrazione
Le soglie cambiano un po' per ogni bastone, ogni superficie e ogni sensore. Prima di passare al codice principale, carica lo sketch qui sotto: ogni quarto di secondo scrive nel monitor seriale la distanza, il valore grezzo del sensore d'acqua, l'accelerazione massima dell'ultimo quarto di secondo e l'inclinazione. Annotare i valori in una tabella ti riempie anche la sezione dei risultati della relazione del progetto.
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}- Misura il sensore d'acqua asciutto e con la punta immersa di 1 cm in un bicchiere d'acqua. Se, per esempio, leggi 0–20 da asciutto e 520 da bagnato, scegli per WATER_LIMIT il valore intermedio di 270–300.
- Tieni il bastone come al solito e muovilo da un lato all'altro come se camminassi. Annota l'inclinazione; di solito resta tra 40 e 55 gradi e deve essere sotto PICKUP_DEG (60).
- Appoggia il bastone piatto a terra. L'inclinazione dovrebbe segnare 85–90 e l'accelerazione circa 0,95–1,05 g, il che conferma LYING_DEG (70) e la finestra di 0,8–1,2 g.
- Batti il bastone a terra come fai di solito camminando e osserva i picchi in g. Poi lascia cadere il bastone 10 volte su un tappetino. Imposta IMPACT_G sotto i valori delle cadute; anche se i colpi superano questa soglia, il controllo dell'inclinazione li scarta.
- Fai 10 letture a 30, 60, 100 e 150 cm da un muro e confronta la media con un metro.
Codice completo: ostacoli, acqua, cadute, SOS e avviso batteria
Nel codice completo i quattro compiti girano nello stesso ciclo senza bloccarsi a vicenda, e il rilevamento cadute è una piccola macchina a stati costruita con enum e switch-case. Il pulsante ha tre funzioni: camminando, una pressione breve attiva o disattiva i bip degli ostacoli (la vibrazione resta sempre attiva), durante un allarme o un SOS una pressione breve lo annulla, e una pressione lunga di 2 secondi avvia un SOS in qualsiasi momento. Un bip acuto indica suono attivo, un bip grave suono spento.
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}| Funzione o impostazione | Cosa fa? |
|---|---|
| pingCm() | Esegue una misura; restituisce 999 se non arriva alcun eco entro 12 ms |
| median3() | Calcola la mediana delle ultime tre letture e scarta un valore errato isolato |
| readMpu() | Legge i sei byte di accelerazione e calcola accelerazione totale e inclinazione |
| fallUpdate() | Esegue la macchina a stati WALK → SETTLE → CHECK → ALARM → SOS |
| waterUpdate() | Alimenta il sensore d'acqua per 10 ms, lo legge e avvia l'avviso nel passaggio da asciutto a bagnato |
| walkOutput() | Genera la vibrazione in base alla distanza e l'avviso d'acqua con un suono a due toni |
| sosOutput() | Riproduce l'array SOS_PATTERN passo dopo passo |
| readButton() | Distingue pressioni brevi (1) e lunghe (2) e ignora i rimbalzi sotto i 30 ms |
| IMPACT_G, LYING_DEG, CANCEL_MS | Sensibilità alle cadute e tempo di annullamento; si regolano con il codice di prova |
Lo schema SOS segue le regole del codice Morse: il punto dura 1 unità, la linea 3 unità, con 1 unità tra i segni, 3 tra le lettere e 7 tra le ripetizioni. Con un'unità di 150 ms, un ciclo SOS dura 34 unità, cioè circa 5,1 secondi. Il tono a 3 kHz è stato scelto apposta: l'orecchio umano è più sensibile tra 2 e 4 kHz, e anche i piccoli buzzer passivi suonano più forte in questa gamma. Per la differenza tra buzzer attivo e passivo, leggi l'articolo Che cos'è un buzzer?.
Batteria, alimentazione e autonomia
Un bastone intelligente si usa all'aperto tutto il giorno, quindi il progetto dell'alimentazione conta quanto i sensori. Stimiamo il consumo medio: il Nano assorbe circa 20 mA, l'HC-SR04 15 mA e l'MPU6050 4 mA. Il motorino vibrante assorbe circa 70 mA, ma solo durante gli impulsi; in una passeggiata tipica la sua media è di circa 10 mA. Buzzer e sensore d'acqua aggiungono pochi mA. In totale fanno circa 50 mA, cioè 0,25 W a 5 V. Una cella 18650 da 2600 mAh immagazzina circa 9,6 Wh a 3,7 V; con un convertitore boost con rendimento dell'85% al circuito arrivano circa 8 Wh. 8 / 0,25 = 32 ore; considerando che non si può usare tutta la batteria, una durata realistica è di 25–30 ore. Chi lo usa due ore al giorno lo ricarica circa ogni due settimane. Puoi fare il tuo calcolo con lo strumento Durata della batteria e leggere la differenza tra mAh e Wh nell'articolo mAh e Wh.
| Fonte di alimentazione | Peso | Autonomia indicativa (50 mA) | Valutazione |
|---|---|---|---|
| Batteria da 9 V (circa 500 mAh), su VIN | 45 g | 8–10 ore | Sconsigliata: la tensione cala quando parte il motore, non è ricaricabile |
| 4 stilo AA NiMH (2000 mAh), sul pin 5V | Circa 120 g | 35–40 ore | Pesante; appena carica arriva a 5,6 V, al limite |
| 1 cella 18650 + TP4056 + MT3608 | Circa 55 g | 25–30 ore | Consigliata: leggera, ricarica USB, protetta |
| 2 celle 18650 in serie (7,4 V), su VIN | Circa 105 g | 45–50 ore | Lunga durata ma pesante, serve un circuito di ricarica a parte |
| Power bank (10.000 mAh) | 200 g e oltre | Diversi giorni | Pesante; molti power bank si spengono con correnti basse |

La catena di alimentazione consigliata è questa: la cella 18650 si collega ai morsetti B+ e B− del modulo TP4056. La linea che esce da OUT+ passa per un interruttore a slitta e arriva all'ingresso del modulo boost MT3608. La sua uscita, regolata a 5,0 V, alimenta il pin 5V del Nano, l'HC-SR04 e il modulo vibrante. Per misurare la tensione della batteria si collega subito dopo l'interruttore un partitore di due resistenze da 100 kΩ, con il punto centrale su A0; trovandosi dopo l'interruttore, non scarica la batteria quando il bastone è spento. Il chip DW01 stacca la cella intorno a 2,4–2,5 V, ma il codice avvisa molto prima con un bip grave ogni 30 secondi sotto i 3,4 V. Dettagli nell'articolo modulo di ricarica TP4056.
Contenitore, peso ed ergonomia
La maggior parte dei bastoni bianchi pieghevoli pesa 200–300 g, e l'utente li muove da un lato all'altro con il polso per ore. Ogni grammo aggiunto si moltiplica per la sua distanza dalla mano e pesa sul polso. Con un contenitore da 150 g fissato 10 cm sotto l'impugnatura, la coppia aggiuntiva sul polso è 150 × 10 = 1,5 kg·cm; lo stesso contenitore 60 cm più in basso darebbe 9 kg·cm, sei volte tanto. Per questo raccogli tutta l'elettronica in un unico contenitore subito sotto l'impugnatura; solo il sensore d'acqua e il suo cavo sottile scendono fino alla punta. Punta a restare sotto i 150 g di peso aggiunto in totale.
| Componente | Peso indicativo |
|---|---|
| Arduino Nano | 7 g |
| HC-SR04 | 9 g |
| MPU6050 (GY-521) | 2 g |
| Modulo vibrante + buzzer passivo | 5 g |
| Sensore di livello dell'acqua e cavo | 8 g |
| Cella 18650 | 45–48 g |
| TP4056 + MT3608 + interruttore | 8 g |
| Contenitore stampato in 3D e fascette | 40–50 g |
| Cavi, viti, strap in velcro | 10–15 g |
| Totale | Circa 135–150 g |
- Materiale: Stampa il contenitore in PETG o ABS; il PLA può ammorbidirsi e deformarsi in un'auto lasciata al sole. Uno spessore di parete di 1,6–2 mm è sufficiente.
- Fissaggio: Fissa il contenitore al bastone con due fascette stringitubo o una strap in velcro rivestita di gomma. Sui bastoni pieghevoli il contenitore deve stare sopra il primo snodo, altrimenti il bastone non si chiude.
- Finestra del sensore: I due cilindri dell'HC-SR04 devono sporgere dal lato anteriore del contenitore; non mettere davanti plastica sottile o rete, perché gli ultrasuoni non passano.
- Pioggia: Pratica un piccolo foro di scarico sul fondo del contenitore e metti dentro una bustina di gel di silice. Se vivi in una zona molto piovosa, usa la sonda impermeabile JSN-SR04T al posto dell'HC-SR04.
- Pulsante: Scegli un pulsante grande con uno scatto ben percepibile e mettilo dove il pollice si appoggia naturalmente sull'impugnatura. Aggiungi un bordo rialzato di 2 mm intorno per evitare pressioni accidentali.
- Accensione: La posizione dell'interruttore deve essere riconoscibile al tatto; anche il bip di accensione conferma che il dispositivo funziona.
- Presa di ricarica: La presa USB del TP4056 deve essere raggiungibile dal fondo del contenitore e chiusa con un tappo di gomma quando non si usa.
- Visibilità: Del nastro catarifrangente sui lati del contenitore aiuta gli automobilisti a notare il bastone quando l'utente attraversa la strada di sera.
Invio della posizione con ESP32 + GPS
Con Arduino, durante un SOS il bastone può avvisare solo chi è vicino, con il suono. Con un ESP32 e un modulo GPS, invece, la posizione del bastone può essere inviata a un familiare con un messaggio. Di default il modulo NEO-6M trasmette frasi NMEA a 9600 baud, con una posizione al secondo; la libreria TinyGPSPlus le decodifica e fornisce latitudine, longitudine e numero di satelliti. Secondo il datasheet, il primo fix dopo un avvio a freddo richiede circa 27 secondi; in pratica, all'aperto servono da 30 secondi a qualche minuto, mentre al chiuso spesso il fix non arriva affatto. La piccola batteria tampone sul modulo conserva gli ultimi dati dei satelliti, così agli avvii successivi la posizione arriva in pochi secondi. Dettagli nell'articolo modulo GPS NEO-6M.

Ci sono due modi per inviare il messaggio. Per una dimostrazione a scuola, il più semplice è collegare l'ESP32 all'hotspot personale del telefono dell'utente e inviare il messaggio con un bot Telegram. Per un bastone che funzioni davvero per strada è più adatto un modulo GSM SIM800L, che non dipende dal telefono: invia direttamente un SMS con la SIM inserita. Il SIM800L richiede 3,4–4,4 V e durante la trasmissione assorbe picchi di corrente fino a 2 A; alimentalo quindi direttamente dalla cella 18650 e non dal boost, e metti un condensatore da 1000 µF sui pin di alimentazione. Questo modulo funziona solo sulle reti 2G; nei paesi in cui il 2G è stato spento serve un modulo 4G come l'A7670 o il SIM7600. Se vuoi tutto su una sola scheda, puoi usare una 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}Il codice usa la stessa macchina a stati della versione Arduino; la differenza è nell'SOS. Appena parte l'SOS, sendSms() invia al SIM800L in sequenza i comandi AT, AT+CMGF=1 per la modalità testo e AT+CMGS="numero", attende il prompt ">", scrive il messaggio e lo invia con Ctrl+Z (26). Nel messaggio la posizione compare come link a una mappa, che sul telefono si apre con un tocco nell'app delle mappe. Se il GPS non ha ancora un fix, il messaggio lo indica, e finché l'SOS continua la posizione viene inviata di nuovo ogni 5 minuti. Il testo dell'SMS evita di proposito le lettere turche: caratteri come ş, ğ e ı non fanno parte dell'alfabeto GSM standard e su alcuni telefoni appaiono storpiati. Durante l'invio il codice può attendere fino a 30 secondi; dopo l'accensione, inoltre, il SIM800L impiega 10–30 secondi per registrarsi sulla rete.
Bastone intelligente con il Raspberry Pi Pico (MicroPython)
Sul Raspberry Pi Pico la stessa logica si programma in MicroPython. time_pulse_us() funziona come pulseIn() su Arduino e restituisce un valore negativo se non arriva l'eco. Le uscite PWM si impostano con duty_u16(), a 16 bit; per il motorino vibrante 65535 significa piena potenza. I sei byte dell'MPU6050 si leggono in un colpo solo con i2c.readfrom_mem() e si convertono in numeri con segno sottraendo 65536 ai valori maggiori di 32767. Anche i pin del Pico lavorano a 3,3 V, quindi il partitore su ECHO è indispensabile pure qui. A batteria, porta l'uscita a 5 V dell'MT3608 sul pin VSYS; l'HC-SR04 si alimenta dalla stessa linea a 5 V. Con il Pico W, che ha il Wi-Fi, puoi anche mostrare lo stato del bastone su una pagina web.
1from machine import Pin, PWM, I2C, ADC, time_pulse_us
2import time, math
3
4# Pinler: ECHO 1 kΩ / 2 kΩ bölücüyle / Pins: ECHO via a 1 kΩ / 2 kΩ divider
5trig, echo = Pin(2, Pin.OUT), Pin(3, Pin.IN)
6i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400000) # MPU6050
7button = Pin(14, Pin.IN, Pin.PULL_UP)
8vib = PWM(Pin(15))
9vib.freq(1000)
10buzzer = PWM(Pin(16))
11water_pwr, water_sig = Pin(22, Pin.OUT), ADC(26)
12bat = ADC(28) # 100k/100k bölücü / 100k/100k divider
13
14NEAR_CM, FAR_CM, PULSE_MS = 30, 150, 60
15WATER_LIMIT = 20000 # 16 bit ölçekte, test ederek bul / on the 16-bit scale, find by testing
16IMPACT_G, LYING_DEG, PICKUP_DEG, CANCEL_MS = 2.5, 70, 60, 20000
17SOS = [1, -1, 1, -1, 1, -3, 3, -1, 3, -1, 3, -3, 1, -1, 1, -1, 1, -7] # Mors kodu / Morse code
18UNIT_MS = 150
19MPU = 0x68
20
21i2c.writeto_mem(MPU, 0x6B, b'\x00') # uyandır / wake up
22i2c.writeto_mem(MPU, 0x1A, b'\x03') # 44 Hz filtre / 44 Hz filter
23i2c.writeto_mem(MPU, 0x1C, b'\x10') # ±8 g
24
25beep_off = None
26
27def tone(freq, ms=0): # ms = 0: noTone'a kadar çalar / plays until quiet()
28 global beep_off
29 buzzer.freq(freq)
30 buzzer.duty_u16(32768) # %50 doluluk / 50% duty
31 beep_off = time.ticks_add(time.ticks_ms(), ms) if ms else None
32
33def quiet():
34 global beep_off
35 buzzer.duty_u16(0)
36 beep_off = None
37
38def ping_cm():
39 trig.low(); time.sleep_us(2)
40 trig.high(); time.sleep_us(10) # 10 µs tetik / 10 µs trigger
41 trig.low()
42 t = time_pulse_us(echo, 1, 12000) # yankı yoksa negatif / negative if no echo
43 return 999 if t < 0 else t // 58
44
45def read_mpu():
46 d = i2c.readfrom_mem(MPU, 0x3B, 6)
47 a = []
48 for i in (0, 2, 4):
49 v = (d[i] << 8) | d[i + 1]
50 if v > 32767:
51 v -= 65536 # işaretli 16 bit / signed 16-bit
52 a.append(v / 4096) # ±8 g: 4096 LSB/g
53 g = math.sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2])
54 tilt = math.atan2(math.sqrt(a[1] * a[1] + a[2] * a[2]), abs(a[0])) * 180 / math.pi
55 return g, tilt
56
57mode, mode_t = "walk", time.ticks_ms()
58sound_on, wet, water_alert, water_high = True, False, False, False
59hist, hist_i, dist = [999, 999, 999], 0, 999
60sos_i, sos_t, sos_on = 0, 0, None
61btn_down, long_fired, btn_t = False, False, 0
62last_ping = last_pulse = last_mpu = last_water = last_bat = last_beep = water_t = time.ticks_ms()
63
64def set_mode(m):
65 global mode, mode_t, sos_i, sos_t, sos_on
66 mode, mode_t = m, time.ticks_ms()
67 sos_i, sos_t, sos_on = 0, mode_t, None
68 vib.duty_u16(0)
69 quiet()
70
71def button_event(now): # 0 yok, 1 kısa, 2 uzun / 0 none, 1 short, 2 long
72 global btn_down, long_fired, btn_t
73 pressed = button.value() == 0
74 if pressed and not btn_down:
75 btn_down, long_fired, btn_t = True, False, now
76 if pressed and btn_down and not long_fired and time.ticks_diff(now, btn_t) >= 2000:
77 long_fired = True
78 return 2
79 if not pressed and btn_down:
80 btn_down = False
81 if not long_fired and time.ticks_diff(now, btn_t) >= 30:
82 return 1
83 return 0
84
85tone(1500, 100) # açılış sesi / power-on beep
86while True:
87 now = time.ticks_ms()
88 if beep_off is not None and time.ticks_diff(now, beep_off) >= 0:
89 quiet()
90
91 b = button_event(now)
92 if b == 2:
93 set_mode("sos") # uzun basış: elle SOS / long press: manual SOS
94 elif b == 1:
95 if mode in ("alarm", "sos"):
96 set_mode("walk") # alarmı ya da SOS'u iptal et / cancel the alarm or SOS
97 else: # yürürken: ses aç/kapat / while walking: sound on/off
98 sound_on = not sound_on
99 tone(2000 if sound_on else 600, 120)
100
101 if time.ticks_diff(now, last_mpu) >= 20: # 50 Hz
102 last_mpu = now
103 g, tilt = read_mpu()
104 el = time.ticks_diff(now, mode_t)
105 if mode == "walk" and g > IMPACT_G:
106 set_mode("settle")
107 elif mode == "settle" and el >= 1000:
108 set_mode("check")
109 elif mode == "check":
110 if tilt < LYING_DEG or abs(g - 1) > 0.2:
111 set_mode("walk") # yalnızca bir vuruş / just a tap
112 elif el >= 2000:
113 set_mode("alarm")
114 elif mode == "alarm":
115 if tilt < PICKUP_DEG:
116 set_mode("walk") # baston kaldırıldı / cane picked up
117 elif el >= CANCEL_MS:
118 set_mode("sos")
119
120 if mode in ("walk", "settle", "check"):
121 if time.ticks_diff(now, last_ping) >= 60:
122 last_ping = now
123 hist[hist_i] = ping_cm()
124 hist_i = (hist_i + 1) % 3
125 dist = sorted(hist)[1] # 3 ölçümün ortancası / median of 3
126 if time.ticks_diff(now, last_water) >= 500:
127 last_water = now
128 water_pwr.on()
129 time.sleep_ms(10)
130 w = water_sig.read_u16()
131 water_pwr.off()
132 if w > WATER_LIMIT and not wet:
133 wet, water_alert, water_t = True, True, now
134 elif w < WATER_LIMIT - 5000:
135 wet = False
136 if water_alert and time.ticks_diff(now, water_t) >= 1800:
137 water_alert = False
138 if water_alert and time.ticks_diff(now, last_beep) >= 300:
139 last_beep = now
140 water_high = not water_high
141 tone(1600 if water_high else 1000, 250) # iki tonlu su uyarısı / two-tone water alert
142 if dist < 2 or dist > FAR_CM:
143 vib.duty_u16(0)
144 elif dist <= NEAR_CM:
145 vib.duty_u16(65535)
146 if sound_on and not water_alert and time.ticks_diff(now, last_beep) >= 120:
147 last_beep = now
148 tone(2500, 60)
149 else:
150 period = 120 + (dist - NEAR_CM) * 780 // (FAR_CM - NEAR_CM) # 120–900 ms
151 strength = 65535 - (dist - NEAR_CM) * 27000 // (FAR_CM - NEAR_CM) # %100–%59
152 if time.ticks_diff(now, last_pulse) >= period:
153 last_pulse = now
154 if sound_on and not water_alert:
155 tone(2000, 30)
156 vib.duty_u16(strength if time.ticks_diff(now, last_pulse) < PULSE_MS else 0)
157 elif mode == "alarm":
158 vib.duty_u16(65535 if time.ticks_diff(now, mode_t) % 1000 < 150 else 0)
159 if time.ticks_diff(now, last_beep) >= 1000:
160 last_beep = now
161 tone(2000, 150) # bulma sesi / locator beep
162 else:
163 if time.ticks_diff(now, sos_t) >= abs(SOS[sos_i]) * UNIT_MS:
164 sos_i = (sos_i + 1) % len(SOS)
165 sos_t = now
166 on = SOS[sos_i] > 0
167 if on != sos_on:
168 sos_on = on
169 if on:
170 tone(3000)
171 else:
172 quiet()
173 vib.duty_u16(65535 if on else 0)
174
175 if time.ticks_diff(now, last_bat) >= 30000:
176 last_bat = now
177 v = bat.read_u16() * 3.3 / 65535 * 2
178 print("Pil / Battery:", round(v, 2), "V")
179 if v < 3.4 and mode == "walk":
180 tone(800, 400)
181
182 time.sleep_ms(2)Consigli per presentarlo a una fiera della scienza o a un concorso
Il bastone intelligente è un progetto frequente nelle fiere della scienza e nei concorsi scolastici (trovi altre idee nel nostro articolo idee di progetti per le fiere della scienza), quindi ciò che colpisce la giuria non è che il bastone vibri, ma quanto bene definisci il problema e quanto onestamente misuri la tua soluzione. I progetti migliori iniziano con una chiacchierata con una persona cieca o con un'associazione: sentire da loro quali ostacoli sono davvero un problema, se preferiscono la vibrazione o il suono e quanto può pesare al massimo il bastone dà al tuo progetto una direzione e una storia forte.
- Problema: Il bastone bianco non rileva gli ostacoli sopra la vita e le pozzanghere passano inosservate. Spiegalo con esempi concreti tratti dalle tue interviste.
- Obiettivo: Progettare un accessorio che segnali con la vibrazione, con almeno 1 secondo di anticipo, gli ostacoli all'altezza del corpo e della testa, senza cambiare troppo il peso e l'uso del bastone.
- Metodo: Mostra la scelta del sensore (HC-SR04, JSN-SR04T, ToF) con le motivazioni, l'angolo di montaggio con il calcolo e la mappatura della vibrazione con la formula.
- Risultati: Presenta i risultati delle prove qui sotto con tabelle e grafici e riporta anche i casi in cui ha fallito.
- Conclusioni e proposte: Indica chiaramente cosa risolve il progetto, i suoi limiti (vetro, superfici morbide, pioggia, rilevamento della caduta del bastone invece che della persona) e il passo successivo.
| Prova | Come si fa | Valore da annotare |
|---|---|---|
| Precisione della distanza | 10 letture a 30, 60, 100 e 150 cm da un muro | Errore medio (cm) e scostamento massimo |
| Tipi di ostacolo | Muro, persona, palo da 5 cm, cespuglio, porta a vetri, tenda di stoffa | Quante volte su 10 viene rilevato ogni ostacolo |
| Tempo di risposta | Video al rallentatore con il telefono: comparsa dell'ostacolo e prima vibrazione | Ritardo in millisecondi |
| Rilevamento dell'acqua | Immergere la punta in 0,5, 1, 2 e 3 cm d'acqua | Da quale profondità avvisa |
| Rilevamento cadute | 20 cadute e 100 colpi a terra | Numero di allarmi corretti e falsi |
| Autonomia | Farlo funzionare da carica completa fino allo spegnimento | Durata in ore e tensione finale |
| Peso ed equilibrio | Pesare con una bilancia da cucina, misurare la distanza dall'impugnatura | Peso aggiunto (g) e coppia (kg·cm) |
Idee per migliorarlo
- Avviso gradini e buche: Un secondo HC-SR04 rivolto in basso e in avanti vede normalmente il suolo a una distanza costante; se la lettura si allunga all'improvviso, davanti c'è un gradino in discesa o una buca.
- Messaggi vocali: Con un DFPlayer Mini puoi riprodurre in un auricolare frasi registrate come "ostacolo davanti", "acqua" o "batteria scarica" al posto dei bip.
- App sul telefono: Usa il Bluetooth BLE dell'ESP32 per mostrare batteria e impostazioni sul telefono e cambiare la portata da lì.
- Direzione: Metti due motorini, a sinistra e a destra dell'impugnatura, e indica con la vibrazione da che lato si trova l'ostacolo.
- Modalità riposo: Spegni i sensori quando l'MPU6050 vede che il bastone non si muove da minuti; con il deep sleep dell'ESP32 l'autonomia si moltiplica.
- Visibilità notturna: Con una LDR, fai lampeggiare un LED sulla punta del bastone quando fa buio, così gli automobilisti notano l'utente da lontano.
Errori frequenti e soluzioni
| Sintomo | Causa probabile | Soluzione |
|---|---|---|
| Il bastone vibra senza sosta | Il sensore punta al suolo o al bastone stesso | Aumenta l'angolo di montaggio e lascia libero lo spazio davanti al sensore |
| Vibrazioni sporadiche senza motivo | Un eco errato isolato o un'altra sorgente di ultrasuoni vicina | Il filtro mediano c'è già; se serve, usa la mediana di 5 letture |
| A grandi distanze il motore non gira | Il valore PWM è sotto la soglia di avvio del motore | Tieni la forza minima sopra 150 e fissa il motore con la gomma |
| Arduino si riavvia quando parte il motore | La corrente del motore fa crollare l'alimentazione o manca il diodo | Usa un modulo pronto e aggiungi un condensatore da 100 µF sull'alimentazione |
| L'MPU6050 legge sempre 0 o un valore fisso | SDA e SCL invertiti, indirizzo 0x69 o chip in sospensione | Trova l'indirizzo con uno scanner I2C e verifica che nel registro 0x6B venga scritto 0 |
| Falsi allarmi battendo il bastone a terra | IMPACT_G basso e soglia di inclinazione vicina all'angolo di cammino | Misura l'inclinazione con il codice di prova e porta LYING_DEG a 75 |
| L'avviso d'acqua non smette mai o non suona mai | Soglia sbagliata o piste ossidate | Rimisura i valori da asciutto e da bagnato e sostituisci il sensore se serve |
| Il buzzer è muto o suona sempre lo stesso tono | È montato un buzzer attivo | Usa un buzzer passivo per tone() |
| L'SMS non parte | Il SIM800L non è registrato sulla rete, l'alimentazione è debole o il PIN della SIM è attivo | Controlla che il LED del modulo lampeggi ogni 3 secondi, disattiva il PIN e alimenta il modulo dalla 18650 |
Un bastone intelligente riunisce in un solo progetto la misura della distanza, il feedback aptico, il rilevamento di eventi con un sensore di movimento e il progetto dell'alimentazione, e poiché risponde a un bisogno reale è uno dei progetti Arduino più sensati che tu possa realizzare. Parti dal codice di base di questa guida, compila le tue tabelle di prova e poi aggiungi uno alla volta i livelli acqua, cadute e GPS. Nell'app BlueGrays trovi, passo dopo passo e con schemi e codice pronto, progetti di misura della distanza con l'HC-SR04, un sensore di parcheggio, suoni con il buzzer e notifiche con l'ESP32, così puoi provare prima ogni parte del tuo bastone da sola.
Domande frequenti
Come funziona un bastone intelligente per non vedenti?
Un sensore a ultrasuoni HC-SR04 montato vicino all'impugnatura emette onde sonore e calcola la distanza dall'ostacolo in base al tempo di ritorno dell'eco. A seconda di questa distanza, Arduino aziona un motorino vibrante nell'impugnatura: più l'ostacolo è vicino, più la vibrazione è rapida e forte. Nelle versioni avanzate un sensore d'acqua sulla punta rileva le pozzanghere e un MPU6050 rileva la caduta del bastone.
Quali componenti servono per un bastone intelligente con Arduino?
Per la versione di base bastano un Arduino Nano, un sensore a ultrasuoni HC-SR04, un modulo motorino vibrante, un buzzer passivo e dei cavetti. La versione completa aggiunge un sensore di livello dell'acqua, un MPU6050, un pulsante e una cella 18650 con i moduli di ricarica TP4056 e boost MT3608. Per inviare la posizione servono un ESP32, un GPS NEO-6M e un modulo GSM SIM800L.
A che distanza un bastone intelligente rileva gli ostacoli?
L'HC-SR04 misura da 2 a 400 cm, ma su un bastone si usa di solito un limite di 150 cm. Per chi cammina a circa 1 metro al secondo, questo significa un preavviso di 1,5 secondi prima di raggiungere l'ostacolo. Una portata molto maggiore provoca vibrazioni continue nei luoghi affollati e porta l'utente a ignorare gli avvisi.
Come fa un bastone intelligente a capire che è caduto?
L'accelerometro MPU6050 viene letto 50 volte al secondo. Se dopo un urto superiore a 2,5 g il bastone resta orizzontale e immobile per 2 secondi, viene considerato caduto e parte un bip per ritrovarlo. Se entro 20 secondi nessuno lo solleva o preme il pulsante, suona l'SOS; la versione con ESP32 in quel momento invia la posizione via SMS.
Quanto dura la batteria di un bastone intelligente con Arduino?
Il circuito assorbe in media circa 50 mA. Una sola cella 18650 da 2600 mAh con i moduli TP4056 e MT3608 garantisce circa 25–30 ore, cioè circa due settimane con due ore di uso al giorno. Una batteria da 9 V dura solo 8–10 ore ed è sconsigliata, perché la sua tensione cala appena parte il motore.
Si può aggiungere il GPS a un bastone intelligente per inviare la posizione?
Sì. Se usi un ESP32 al posto di Arduino e aggiungi un modulo GPS NEO-6M, puoi leggere latitudine e longitudine del bastone. Con un modulo GSM SIM800L, durante un SOS la posizione arriva a un familiare via SMS come link a una mappa; se c'è il Wi-Fi funziona anche un bot Telegram. Il GPS lavora all'aperto, ma al chiuso di solito non trova la posizione.
Progetti da realizzare dopo questo articolo
Vedi tutti i progetti
HC-SR04 Misura della distanza (display seriale)
Il sensore HC-SR04 funziona come un pipistrello che trova la strada al buio: manda un suono che non puoi sentire, ne ascolta l'eco e misura quanto è lontano l'oggetto davanti a sé. L'Arduino scrive qu

Uso del cicalino
Ti sei mai chiesto come fa un campanello o un allarme a suonare? L'Arduino fa suonare un piccolo cicalino a intervalli brevi: bip, pausa, bip, pausa. Vedi come l'elettricità diventa suono e fai il pri

Impostazione della frequenza con cicalino
Comandi il suono di un buzzer con quattro pulsanti: uno avvia il suono, uno lo zittisce e gli altri due lo rendono più acuto o più grave. Tieni premuto il pulsante su o giù e il tono cambia passo dopo

Sensore di parcheggio con HC-SR04
Una versione in miniatura del sensore di parcheggio delle auto: più qualcosa si avvicina, più i bip accelerano. Il sensore di distanza misura l'oggetto davanti a sé; se è a più di 20 centimetri resta

ESP32 HC-SR04 Misuratore di distanza a ultrasuoni
Il sensore a ultrasuoni HC-SR04 calcola la distanza misurando il tempo di volo (Time of Flight) dell'onda sonora che emette dopo aver colpito un oggetto e essere rimbalzata indietro. In questo progetto invieremo un impulso di 10 microsecondi dall'ESP32 al pin TRIG del sensore e leggeremo il tempo di eco proveniente dal pin ECHO con pulseIn(). Visualizziamo la distanza calcolata in cm su un display LCD I2C 16x2. Poiché l'HC-SR04 funziona a 5V, è necessario un divisore di tensione sul pin EC

Sensore di parcheggio ESP32 (HC-SR04 + barra a 5 LED + cicalino + OLED)
La versione ESP32 del sensore di parcheggio per auto! Grazie al sensore a ultrasuoni HC-SR04 misuriamo la distanza dall'oggetto davanti al veicolo. Man mano che la distanza diminuisce, la barra composta da 5 LED si riempie (da verde → giallo → rosso) e l'intervallo tra i bip del cicalino si accorcia. Quando ci si avvicina troppo (meno di 10 cm), l'allarme suona ininterrottamente e tutti i LED si accendono. Sul display OLED SSD1306 vengono visualizzati in caratteri grandi il valore istantaneo in

Robot che evita gli ostacoli
Che ne dite di costruire un veicolo intelligente che trova la propria strada? L'Obstacle Avoiding Robot scruta costantemente l'ambiente circostante grazie al sensore a ultrasuoni posto davanti a lui.

Sistema di Allarme Movimento ESP32 (PIR + Buzzer + LED + Telegram)
Un sistema di allarme di sicurezza intelligente che attiva buzzer e LED quando il sensore PIR rileva movimento, inviando contemporaneamente una notifica istantanea al proprietario tramite Telegram. Dispone di un tempo di attesa di 30 secondi per limitare la frequenza degli allarmi ripetuti; questo meccanismo non verifica la validità dei rilevamenti del sensore PIR né elimina le false attivazioni. Adatto a sicurezza domestica, monitoraggio di garage, magazzini o studi. Con l'ESP32-CAM è possibile
Altri articoli
App BlueGrays
Costruisci questo circuito passo passo sul telefono
Schema, codice ed elenco componenti in un’unica schermata. Oltre 200 progetti Arduino, ESP32 e Pico da realizzare.
Inquadra con il telefono
Punta la fotocamera sul codice: si apre lo store del tuo telefono.
oppure apri lo store

