Guide · 26 min di lettura

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.

Sensore di distanza a ultrasuoni HC-SR04 per bastone intelligente
L'HC-SR04 invia un impulso sonoro a 40 kHz e calcola la distanza dal tempo di ritorno dell'eco. Sul bastone intelligente controlla gli ostacoli tra 2 e 150 cm dentro un cono stretto di circa 15 gradi.

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.

ComponenteFunzioneNota
Arduino Nano (Uno per il prototipo)Misura, decide, comanda motore e buzzerIl Nano è adatto al bastone perché è piccolo e leggero
Sensore a ultrasuoni HC-SR04Misura la distanza dall'ostacolo davanti5 V, circa 15 mA; JSN-SR04T per la pioggia
Modulo motorino vibranteSegnala l'ostacolo alla mano con la vibrazioneModulo pronto con transistor e diodo
Buzzer passivoAvvisi di acqua, caduta, SOS e batteriaPassivo per poter suonare toni diversi
Sensore di livello dell'acquaAvvisa quando la punta entra in acqua1–2 cm sopra la punta, alimentato solo durante la misura
MPU6050 (GY-521)Capisce che il bastone è caduto ed è a terraI2C, indirizzo 0x68
PulsanteSuono sì/no, annulla allarme, SOS con pressione lungaPosizionato sotto il pollice
Cella 18650 + TP4056 + MT3608Alimentazione portatile, ricarica USB, innalzamento a 5 VUsa un TP4056 protetto (con DW01)
2 resistenze da 100 kΩPartitore per misurare la tensione della batteriaIl punto centrale va ad A0
Interruttore e contenitoreAccensione/spegnimento e protezioneStampato in 3D o una piccola scatola per progetti
Arduino Nano
Arduino Nano
Buzzer passivo
Buzzer passivo
Pulsante
Pulsante
Batteria Li-ion 18650
Batteria Li-ion 18650

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 mOstacoli tipici rilevati
0°Circa 55–95 cmPanchine, bidoni, la carrozzeria di un'auto parcheggiata
15°Circa 95–135 cmIl busto di una persona, il manubrio di una bici, il cassone di un camion
25°Circa 120–170 cmCartelli, 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.

SensorePortataPregiDifetti
HC-SR042–400 cmEconomico, semplice, cono ampioNon impermeabile, fatica con le superfici morbide
JSN-SR04TCirca 25–450 cmSonda impermeabile, funziona sotto la pioggiaNon vede sotto i 25 cm, più costoso
VL53L1X (laser ToF)Fino a 4 mPiccolo, leggero, fascio precisoPortata ridotta alla luce del sole, I2C a 3,3 V
Sharp GP2Y0A02 (infrarossi)20–150 cmUscita analogica, codice sempliceSi inganna con superfici scure e lucide, assorbe circa 33 mA
Conosci i punti ciechi del sensore a ultrasuoni: le superfici morbide come un cappotto pesante, un cespuglio o una tenda assorbono il suono, mentre una porta a vetri o un pannello inclinato lo deviano altrove, e l'ostacolo può non comparire affatto. Anche i pali sottili da 3–4 cm vengono notati tardi perché sono piccoli dentro il cono. Per questo non presentare mai un bastone intelligente come unico ausilio di sicurezza: è un assistente che lavora insieme al bastone bianco.

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.

DistanzaIntervallo tra impulsiForza della vibrazione (PWM)Significato per l'utente
Oltre 150 cmNessuna vibrazione0Strada libera
150 cm900 ms150C'è un ostacolo più avanti
100 cm575 ms194L'ostacolo si avvicina, attenzione
60 cm315 ms229Rallenta, preparati a cambiare direzione
40 cm185 ms247Molto vicino
30 cm o menoContinua255Fermati
Modulo motorino vibrante per Arduino
Modulo motorino vibrante: accanto al motore a moneta, di circa 10 mm di diametro, ci sono il transistor che lo pilota e un diodo di ricircolo. Il pin IN va direttamente a un pin PWM di Arduino e la corrente del motore la gestisce il modulo.

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.

CollegamentoArduino Uno / NanoESP32Raspberry Pi Pico
HC-SR04 VCC5VVIN (5 V)VBUS o linea a 5 V
HC-SR04 TRIGD9GPIO 18GP2
HC-SR04 ECHOD10 (diretto)GPIO 19, con partitore 1 kΩ / 2 kΩGP3, con partitore 1 kΩ / 2 kΩ
Modulo vibrante IND5 (PWM)GPIO 25GP15
Buzzer passivo (+)D8GPIO 26GP16
Pulsante (altro piedino a GND)D2GPIO 27GP14
Sensore d'acqua +D7GPIO 33GP22
Sensore d'acqua SA1GPIO 34 (ADC1)GP26 (ADC0)
MPU6050 SDAA4GPIO 21GP4
MPU6050 SCLA5GPIO 22GP5
MPU6050 VCC5V3V33V3
Misura batteria (centro del partitore 100k/100k)A0GPIO 35 (ADC1)GP28 (ADC2)
GPS NEO-6M TX / RXNessunoGPIO 16 / GPIO 17Nessuno
SIM800L TXD / RXDNessunoGPIO 13 / GPIO 23Nessuno
Il pin ECHO dell'HC-SR04 fornisce un impulso a 5 V, ma i pin dell'ESP32 e del Pico lavorano a 3,3 V e non tollerano i 5 V. Costruisci un partitore di tensione secondo lo schema ECHO → 1 kΩ → filo verso la scheda → 2 kΩ → GND, e al pin arrivano circa 3,3 V. Sull'ESP32 usa i pin dell'ADC1 (GPIO 32–39) per le letture analogiche, perché l'ADC2 non funziona con il Wi-Fi attivo. Sulle schede ESP32 con modulo WROVER, i GPIO 16 e 17 sono collegati alla PSRAM; se hai una scheda così, sposta il GPS su altri due pin. Tutti i moduli devono avere la massa (GND) in comune.

Montaggio passo passo: prima il bastone di base

  1. 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.
  2. Collega VCC e GND dell'HC-SR04 alle linee laterali, TRIG a D9 ed ECHO a D10.
  3. 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.
  4. 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.
  5. 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.
  6. Quando tutto funziona, aggiungi il sensore d'acqua, l'MPU6050 e il pulsante e passa al codice completo.
C++ (Arduino IDE)
1// Temel akıllı baston: engel yaklaştıkça titreşim ve bip hızlanır / Basic smart cane: vibration and beeps speed up as an obstacle gets closer
2const int TRIG = 9, ECHO = 10;      // HC-SR04
3const int VIB = 5;                  // titreşim modülü IN, PWM pini / vibration module IN, PWM pin
4const int BUZZER = 8;               // pasif buzzer / passive buzzer
5
6const int NEAR_CM = 30;             // bundan yakın: sürekli uyarı / closer than this: continuous alert
7const int FAR_CM = 150;             // bundan uzak: sessiz / farther than this: silent
8const int PULSE_MS = 60;            // bir titreşim darbesinin süresi / length of one vibration pulse
9
10unsigned long lastPing = 0, lastPulse = 0;
11int distanceCm = 999;
12int vibNow = -1;
13
14void setVib(int v) {                // yalnızca değer değişince yaz / write only when the value changes
15  if (v != vibNow) { analogWrite(VIB, v); vibNow = v; }
16}
17
18int pingCm() {
19  digitalWrite(TRIG, LOW);  delayMicroseconds(2);
20  digitalWrite(TRIG, HIGH); delayMicroseconds(10);   // 10 µs tetik darbesi / 10 µs trigger pulse
21  digitalWrite(TRIG, LOW);
22  unsigned long t = pulseIn(ECHO, HIGH, 12000UL);    // 12 ms ≈ 2 m, daha uzağı gerekmez / 12 ms ≈ 2 m, no need for more
23  return t == 0 ? 999 : (int)(t / 58);               // 999 = yankı yok / 999 = no echo
24}
25
26void setup() {
27  Serial.begin(9600);
28  pinMode(TRIG, OUTPUT);
29  pinMode(ECHO, INPUT);
30  pinMode(VIB, OUTPUT);
31}
32
33void loop() {
34  unsigned long now = millis();
35
36  if (now - lastPing >= 60) {                        // ölçümler arası en az 60 ms / at least 60 ms between pings
37    lastPing = now;
38    distanceCm = pingCm();
39    Serial.println(distanceCm);
40  }
41
42  if (distanceCm < 2 || distanceCm > FAR_CM) {       // yol açık / the path is clear
43    setVib(0);
44  } else if (distanceCm <= NEAR_CM) {                // çok yakın: sürekli / very close: continuous
45    setVib(255);
46    if (now - lastPulse >= 120) { lastPulse = now; tone(BUZZER, 2500, 60); }
47  } else {
48    unsigned long period = map(distanceCm, NEAR_CM, FAR_CM, 120, 900);  // yakın = sık / near = frequent
49    int strength = map(distanceCm, NEAR_CM, FAR_CM, 255, 150);         // yakın = güçlü / near = strong
50    if (now - lastPulse >= period) {
51      lastPulse = now;
52      tone(BUZZER, 2000, 30);                        // kısa bip / short beep
53    }
54    setVib(now - lastPulse < PULSE_MS ? strength : 0);
55  }
56}

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.

Sensore di livello dell'acqua per Arduino
Sensore di livello dell'acqua: quando l'acqua collega le piste di rame intrecciate, la tensione sul pin S sale. Sul bastone intelligente si monta 1–2 cm sopra la punta e si alimenta solo al momento della misura.
Ricopri la parte superiore del sensore senza piste e il connettore con colla a caldo o smalto per unghie; l'acqua deve toccare solo le piste che misurano. Non lasciare il cavo penzolare fuori dal bastone: fallo passare dentro un bastone in alluminio o avvolgilo intorno all'asta dentro una guaina termorestringente. Per una soluzione più robusta, avvita due viti in acciaio inox a 1 cm di distanza appena sopra la punta: una va a D7, l'altra ad A1, collegato a GND tramite una resistenza da 100 kΩ. Quando l'acqua collega le due viti, A1 sale; il codice resta uguale, devi solo rimisurare WATER_LIMIT.

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.

Modulo accelerometro e giroscopio MPU6050 (GY-521)
MPU6050 (GY-521): accelerometro e giroscopio a tre assi. Si fissa nel contenitore con la freccia X stampata sulla scheda allineata all'asta del bastone.

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.

FaseCondizioneDurataCosa succede?
WALK (cammino)L'accelerazione totale supera 2,5 gIllimitataPassa a SETTLE; gli avvisi degli ostacoli continuano
SETTLE (assestamento)Attende che finiscano le scosse1 sPassa a CHECK
CHECK (verifica)L'inclinazione deve restare sopra 70° e l'accelerazione tra 0,8 e 1,2 g2 sSe la condizione salta torna a WALK, se regge parte ALARM
ALARMUn bip e una vibrazione ogni secondo per ritrovare il bastone20 sSi annulla con il pulsante o sollevando il bastone (inclinazione sotto 60°)
SOS... --- ... in codice Morse con suono, LED e vibrazioneFino all'annullamentoUna 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.

Questo metodo rileva la caduta del bastone, non quella della persona. Se qualcuno cade senza lasciare il bastone, o se il bastone resta appoggiato a un muro, l'allarme non scatta; tieni quindi sempre il pulsante SOS manuale e spiega chiaramente questo limite nella tua presentazione. Per misurare direttamente la caduta del corpo, il sensore dovrebbe stare in un dispositivo separato da portare in cintura o al polso.

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.

C++ (Arduino IDE)
1// Test ve kalibrasyon: mesafe, su sensörü ham değeri, en yüksek ivme (g) ve eğim / Test and calibration: distance, raw water value, peak acceleration (g) and tilt
2#include <Wire.h>
3
4const int TRIG = 9, ECHO = 10;
5const int WATER_PWR = 7, WATER_SIG = A1;
6const byte MPU = 0x68;
7
8float peakG = 0, tilt = 0;
9unsigned long lastPrint = 0;
10
11void mpuWrite(byte reg, byte val) {
12  Wire.beginTransmission(MPU);
13  Wire.write(reg);
14  Wire.write(val);
15  Wire.endTransmission();
16}
17
18void readMpu() {
19  Wire.beginTransmission(MPU);
20  Wire.write(0x3B);                                  // ACCEL_XOUT_H
21  if (Wire.endTransmission(false) != 0) return;
22  if (Wire.requestFrom(MPU, (uint8_t)6) != 6) return;
23  float a[3];
24  for (int i = 0; i < 3; i++) {
25    uint8_t hi = Wire.read();
26    uint8_t lo = Wire.read();
27    a[i] = (int16_t)(((uint16_t)hi << 8) | lo) / 4096.0;    // ±8 g: 4096 LSB/g
28  }
29  float g = sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]);
30  if (g > peakG) peakG = g;
31  tilt = atan2(sqrt(a[1] * a[1] + a[2] * a[2]), fabs(a[0])) * 180.0 / PI;  // X ekseni baston boyunca / X axis along the cane
32}
33
34int pingCm() {
35  digitalWrite(TRIG, LOW);  delayMicroseconds(2);
36  digitalWrite(TRIG, HIGH); delayMicroseconds(10);
37  digitalWrite(TRIG, LOW);
38  unsigned long t = pulseIn(ECHO, HIGH, 25000UL);
39  return t == 0 ? 999 : (int)(t / 58);
40}
41
42void setup() {
43  Serial.begin(9600);
44  pinMode(TRIG, OUTPUT);
45  pinMode(ECHO, INPUT);
46  pinMode(WATER_PWR, OUTPUT);
47  Wire.begin();
48  mpuWrite(0x6B, 0x00);                              // uyku modundan çıkar / wake from sleep mode
49  mpuWrite(0x1A, 0x03);                              // 44 Hz alçak geçiren filtre / 44 Hz low-pass filter
50  mpuWrite(0x1C, 0x10);                              // ivme aralığı ±8 g / accelerometer range ±8 g
51}
52
53void loop() {
54  readMpu();                                         // olabildiğince sık oku, tepeyi yakala / read as often as possible to catch the peak
55  if (millis() - lastPrint >= 250) {
56    lastPrint = millis();
57    digitalWrite(WATER_PWR, HIGH);                   // sensörü yalnızca ölçerken besle / power the sensor only while measuring
58    delay(10);
59    int water = analogRead(WATER_SIG);
60    digitalWrite(WATER_PWR, LOW);
61    Serial.print(F("cm: "));          Serial.print(pingCm());
62    Serial.print(F("  su/water: "));  Serial.print(water);
63    Serial.print(F("  tepe/peak g: ")); Serial.print(peakG, 2);
64    Serial.print(F("  egim/tilt: ")); Serial.println(tilt, 0);
65    peakG = 0;
66  }
67}
  1. 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.
  2. 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).
  3. 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.
  4. 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.
  5. 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.

C++ (Arduino IDE)
1// Akıllı baston, tam sürüm: engel, su birikintisi, düşme algılama, SOS ve pil uyarısı
2// Smart cane, full version: obstacles, puddles, fall detection, SOS and low-battery warning
3#include <Wire.h>
4
5// Pinler (Arduino Uno ve Nano) / Pins (Arduino Uno and Nano)
6const int BUTTON = 2;               // buton, diğer bacağı GND / button, other leg to GND
7const int VIB = 5;                  // titreşim modülü IN (PWM) / vibration module IN (PWM)
8const int WATER_PWR = 7;            // su sensörü + ucu / water sensor + pin
9const int BUZZER = 8;               // pasif buzzer / passive buzzer
10const int TRIG = 9, ECHO = 10;      // HC-SR04
11const int LED = 13;                 // dahili LED, SOS'ta yanıp söner / built-in LED, flashes during SOS
12const int BAT = A0;                 // 18650, 100k/100k bölücüyle / 18650 via a 100k/100k divider
13const int WATER_SIG = A1;           // su sensörü S ucu / water sensor S pin
14const byte MPU = 0x68;              // MPU6050, AD0 = GND
15
16// Ayarlar: test koduyla bulduğun değerleri yaz / Settings: use the values from the test sketch
17const int NEAR_CM = 30, FAR_CM = 150;
18const int PULSE_MS = 60;
19const int WATER_LIMIT = 300;        // kuru ve ıslak okumaların ortası / midpoint of dry and wet readings
20const float IMPACT_G = 2.5;         // düşme darbesi eşiği / fall impact threshold
21const float LYING_DEG = 70;         // bundan büyük eğim = yerde yatıyor / tilt above this = lying flat
22const float PICKUP_DEG = 60;        // bundan küçük eğim = kaldırıldı / tilt below this = picked up
23const unsigned long CANCEL_MS = 20000;   // SOS'tan önce iptal süresi / time to cancel before SOS
24const float LOW_BAT_V = 3.4;        // 18650 için düşük pil sınırı / low limit for an 18650 cell
25
26enum Mode { WALK, SETTLE, CHECK, ALARM, SOS };
27Mode mode = WALK;
28
29// SOS Mors kodu: artı = ses, eksi = sessizlik, birim 150 ms / SOS in Morse: plus = tone, minus = silence, unit 150 ms
30const int8_t SOS_PATTERN[] = {1, -1, 1, -1, 1, -3, 3, -1, 3, -1, 3, -3, 1, -1, 1, -1, 1, -7};
31const byte SOS_LEN = sizeof(SOS_PATTERN) / sizeof(SOS_PATTERN[0]);
32const unsigned long UNIT_MS = 150;
33
34bool soundOn = true, wet = false, waterAlert = false, waterHigh = false, sosOn = false;
35bool btnDown = false, longFired = false;
36int distanceCm = 999, vibNow = -1;
37int hist[3] = {999, 999, 999};
38byte histIdx = 0, sosStep = 0;
39float gTotal = 1.0, tilt = 45;
40unsigned long lastPing = 0, lastPulse = 0, lastMpu = 0, lastWater = 0, lastBat = 0, lastBeep = 0;
41unsigned long modeStart = 0, waterStart = 0, sosStepStart = 0, btnSince = 0;
42
43void setVib(int v) {                // yalnızca değişince yaz / write only on change
44  if (v != vibNow) { analogWrite(VIB, v); vibNow = v; }
45}
46
47void mpuWrite(byte reg, byte val) {
48  Wire.beginTransmission(MPU);
49  Wire.write(reg);
50  Wire.write(val);
51  Wire.endTransmission();
52}
53
54bool readMpu() {
55  Wire.beginTransmission(MPU);
56  Wire.write(0x3B);                                   // ACCEL_XOUT_H
57  if (Wire.endTransmission(false) != 0) return false;
58  if (Wire.requestFrom(MPU, (uint8_t)6) != 6) return false;
59  float a[3];
60  for (int i = 0; i < 3; i++) {
61    uint8_t hi = Wire.read();
62    uint8_t lo = Wire.read();
63    a[i] = (int16_t)(((uint16_t)hi << 8) | lo) / 4096.0;   // ±8 g: 4096 LSB/g
64  }
65  gTotal = sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]);
66  tilt = atan2(sqrt(a[1] * a[1] + a[2] * a[2]), fabs(a[0])) * 180.0 / PI;  // 0° dik, 90° yatay / 0° upright, 90° flat
67  return true;
68}
69
70int pingCm() {
71  digitalWrite(TRIG, LOW);  delayMicroseconds(2);
72  digitalWrite(TRIG, HIGH); delayMicroseconds(10);
73  digitalWrite(TRIG, LOW);
74  unsigned long t = pulseIn(ECHO, HIGH, 12000UL);     // 12 ms ≈ 2 m
75  return t == 0 ? 999 : (int)(t / 58);
76}
77
78int median3(int a, int b, int c) {                    // tek tük hatalı ölçümü eler / drops a single bad reading
79  if ((a <= b && b <= c) || (c <= b && b <= a)) return b;
80  if ((b <= a && a <= c) || (c <= a && a <= b)) return a;
81  return c;
82}
83
84// 0 = olay yok, 1 = kısa basış, 2 = uzun basış (2 s) / 0 = nothing, 1 = short press, 2 = long press (2 s)
85byte readButton(unsigned long now) {
86  bool pressed = digitalRead(BUTTON) == LOW;
87  if (pressed && !btnDown) { btnDown = true; longFired = false; btnSince = now; }
88  if (pressed && btnDown && !longFired && now - btnSince >= 2000) { longFired = true; return 2; }
89  if (!pressed && btnDown) {
90    btnDown = false;
91    if (!longFired && now - btnSince >= 30) return 1;  // 30 ms'den kısası sıçramadır / shorter than 30 ms is bounce
92  }
93  return 0;
94}
95
96void setMode(Mode m, unsigned long now) {
97  mode = m;
98  modeStart = now;
99  sosStep = 0;
100  sosStepStart = now;
101  sosOn = false;
102  setVib(0);
103  noTone(BUZZER);
104  digitalWrite(LED, LOW);
105}
106
107void fallUpdate(unsigned long now) {
108  switch (mode) {
109    case WALK:
110      if (gTotal > IMPACT_G) setMode(SETTLE, now);      // sert darbe / hard impact
111      break;
112    case SETTLE:                                        // 1 s sarsıntının bitmesini bekle / wait 1 s for the shaking to stop
113      if (now - modeStart >= 1000) setMode(CHECK, now);
114      break;
115    case CHECK:                                         // 2 s boyunca yatay ve hareketsiz mi? / flat and still for 2 s?
116      if (tilt < LYING_DEG || fabs(gTotal - 1.0) > 0.2) setMode(WALK, now);   // yalnızca bir vuruştu / just a tap
117      else if (now - modeStart >= 2000) setMode(ALARM, now);
118      break;
119    case ALARM:
120      if (tilt < PICKUP_DEG) setMode(WALK, now);        // baston kaldırıldı / the cane was picked up
121      else if (now - modeStart >= CANCEL_MS) setMode(SOS, now);
122      break;
123    default:
124      break;
125  }
126}
127
128void waterUpdate(unsigned long now) {
129  digitalWrite(WATER_PWR, HIGH);                        // yalnızca ölçerken besle: aşınmayı yavaşlatır / power only while measuring: slows corrosion
130  delay(10);
131  int v = analogRead(WATER_SIG);
132  digitalWrite(WATER_PWR, LOW);
133  if (v > WATER_LIMIT && !wet) { wet = true; waterAlert = true; waterStart = now; }   // kurudan ıslağa / dry to wet
134  else if (v < WATER_LIMIT - 100) wet = false;          // histerezis / hysteresis
135}
136
137void walkOutput(unsigned long now) {
138  // Ses kanalı: önce su uyarısı / Sound channel: water alert first
139  if (waterAlert && now - waterStart >= 1800) waterAlert = false;
140  if (waterAlert && now - lastBeep >= 300) {
141    lastBeep = now;
142    waterHigh = !waterHigh;
143    tone(BUZZER, waterHigh ? 1600 : 1000, 250);         // iki tonlu uyarı / two-tone alert
144  }
145
146  // Titreşim kanalı: engel ne kadar yakınsa o kadar sık ve güçlü / Vibration channel: the closer, the faster and stronger
147  if (distanceCm < 2 || distanceCm > FAR_CM) {
148    setVib(0);
149  } else if (distanceCm <= NEAR_CM) {
150    setVib(255);
151    if (soundOn && !waterAlert && now - lastBeep >= 120) { lastBeep = now; tone(BUZZER, 2500, 60); }
152  } else {
153    unsigned long period = map(distanceCm, NEAR_CM, FAR_CM, 120, 900);
154    int strength = map(distanceCm, NEAR_CM, FAR_CM, 255, 150);
155    if (now - lastPulse >= period) {
156      lastPulse = now;
157      if (soundOn && !waterAlert) tone(BUZZER, 2000, 30);
158    }
159    setVib(now - lastPulse < PULSE_MS ? strength : 0);
160  }
161}
162
163void alarmOutput(unsigned long now) {                   // "bastonum düştü" sesi: bulmayı kolaylaştırır / "cane dropped" sound: helps finding it
164  setVib((now - modeStart) % 1000 < 150 ? 255 : 0);
165  if (now - lastBeep >= 1000) { lastBeep = now; tone(BUZZER, 2000, 150); }
166}
167
168void sosOutput(unsigned long now) {
169  if (now - sosStepStart >= (unsigned long)abs(SOS_PATTERN[sosStep]) * UNIT_MS) {
170    sosStep = (sosStep + 1) % SOS_LEN;                  // sonraki adım / next step
171    sosStepStart = now;
172  }
173  bool on = SOS_PATTERN[sosStep] > 0;
174  if (on != sosOn) {
175    sosOn = on;
176    if (on) tone(BUZZER, 3000); else noTone(BUZZER);
177    digitalWrite(LED, on);
178    setVib(on ? 255 : 0);
179  }
180}
181
182void batteryUpdate() {
183  float v = analogRead(BAT) * (5.0 / 1023.0) * 2.0;     // bölücü gerilimi yarıya indirir / the divider halves the voltage
184  Serial.print(F("Pil / Battery: "));
185  Serial.println(v, 2);
186  if (v < LOW_BAT_V && mode == WALK) tone(BUZZER, 800, 400);   // kalın, uzun bip / low, long beep
187}
188
189void setup() {
190  Serial.begin(9600);
191  pinMode(BUTTON, INPUT_PULLUP);
192  pinMode(TRIG, OUTPUT);
193  pinMode(ECHO, INPUT);
194  pinMode(VIB, OUTPUT);
195  pinMode(WATER_PWR, OUTPUT);
196  pinMode(LED, OUTPUT);
197  Wire.begin();
198  mpuWrite(0x6B, 0x00);                                 // MPU6050'yi uyandır / wake the MPU6050
199  mpuWrite(0x1A, 0x03);                                 // 44 Hz filtre / 44 Hz filter
200  mpuWrite(0x1C, 0x10);                                 // ±8 g
201  tone(BUZZER, 1500, 100);                              // açılış sesi / power-on beep
202}
203
204void loop() {
205  unsigned long now = millis();
206
207  byte b = readButton(now);
208  if (b == 2) {
209    setMode(SOS, now);                                  // uzun basış: elle SOS / long press: manual SOS
210  } else if (b == 1) {
211    if (mode == ALARM || mode == SOS) {
212      setMode(WALK, now);                               // alarmı ya da SOS'u iptal et / cancel the alarm or SOS
213    } else {                                            // yürürken (SETTLE/CHECK dahil) sesi aç ya da kapat / while walking (incl. SETTLE/CHECK) sound on or off
214      soundOn = !soundOn;
215      tone(BUZZER, soundOn ? 2000 : 600, 120);
216    }
217  }
218
219  if (now - lastMpu >= 20) {                            // saniyede 50 ölçüm / 50 readings per second
220    lastMpu = now;
221    if (readMpu()) fallUpdate(now);
222  }
223
224  if (mode == WALK || mode == SETTLE || mode == CHECK) {
225    if (now - lastPing >= 60) {
226      lastPing = now;
227      hist[histIdx] = pingCm();
228      histIdx = (histIdx + 1) % 3;
229      distanceCm = median3(hist[0], hist[1], hist[2]);
230    }
231    if (now - lastWater >= 500) { lastWater = now; waterUpdate(now); }
232    walkOutput(now);
233  } else if (mode == ALARM) {
234    alarmOutput(now);
235  } else {
236    sosOutput(now);
237  }
238
239  if (now - lastBat >= 30000) { lastBat = now; batteryUpdate(); }
240}
Funzione o impostazioneCosa 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_MSSensibilità 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 alimentazionePesoAutonomia indicativa (50 mA)Valutazione
Batteria da 9 V (circa 500 mAh), su VIN45 g8–10 oreSconsigliata: la tensione cala quando parte il motore, non è ricaricabile
4 stilo AA NiMH (2000 mAh), sul pin 5VCirca 120 g35–40 orePesante; appena carica arriva a 5,6 V, al limite
1 cella 18650 + TP4056 + MT3608Circa 55 g25–30 oreConsigliata: leggera, ricarica USB, protetta
2 celle 18650 in serie (7,4 V), su VINCirca 105 g45–50 oreLunga durata ma pesante, serve un circuito di ricarica a parte
Power bank (10.000 mAh)200 g e oltreDiversi giorniPesante; molti power bank si spengono con correnti basse
Modulo di ricarica TP4056 per batterie al litio
Modulo di ricarica TP4056: ricarica la cella 18650 da USB fino a 4,2 V con 1 A. La versione con il chip di protezione DW01 protegge la batteria anche da scarica eccessiva e cortocircuiti; il carico si collega a OUT+ e OUT−.

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.

L'uscita dell'MT3608 si può regolare fino a 28 V con il trimmer, e il modulo può arrivare dalla fabbrica impostato su qualsiasi valore. Prima di collegarlo al Nano, misura l'uscita con il multimetro e gira il trimmer fino a 5,0 V; un boost regolato male distrugge la scheda in pochi secondi. Se durante le prove alimenti la scheda solo da USB, A0 resta flottante, la lettura della batteria è casuale e potresti sentire il bip di batteria scarica; è normale.

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.

ComponentePeso indicativo
Arduino Nano7 g
HC-SR049 g
MPU6050 (GY-521)2 g
Modulo vibrante + buzzer passivo5 g
Sensore di livello dell'acqua e cavo8 g
Cella 1865045–48 g
TP4056 + MT3608 + interruttore8 g
Contenitore stampato in 3D e fascette40–50 g
Cavi, viti, strap in velcro10–15 g
TotaleCirca 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.

Modulo GPS NEO-6M con antenna ceramica
Modulo GPS NEO-6M: l'antenna ceramica deve vedere il cielo, quindi va sulla parte superiore del contenitore, lontano dalle parti metalliche. La velocità predefinita è 9600 baud.

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).

C++ (Arduino IDE)
1// ESP32 akıllı baston: engel uyarısı, su ve düşme algılama, SOS'ta GPS konumunu SMS ile gönderme
2// ESP32 smart cane: obstacle alert, water and fall detection, sends the GPS location by SMS on SOS
3// Kütüphane: TinyGPSPlus (Mikal Hart) / Library: TinyGPSPlus (Mikal Hart)
4#include <Wire.h>
5#include <TinyGPSPlus.h>
6
7// Pinler / Pins
8const int TRIG = 18, ECHO = 19;          // ECHO 1 kΩ / 2 kΩ bölücüyle / ECHO via a 1 kΩ / 2 kΩ divider
9const int VIB = 25, BUZZER = 26, BUTTON = 27;
10const int WATER_PWR = 33, WATER_SIG = 34;   // ADC1 pini / ADC1 pin
11const int BAT = 35;                      // 100k/100k bölücü / 100k/100k divider
12const int GPS_RX = 16, GPS_TX = 17;      // NEO-6M TX → 16, RX ← 17
13const int GSM_RX = 13, GSM_TX = 23;      // SIM800L TXD → 13, RXD ← 23
14const byte MPU = 0x68;                   // SDA 21, SCL 22
15
16const char* PHONE = "+905XXXXXXXXX";     // SMS gidecek numara / number to text
17
18const int NEAR_CM = 30, FAR_CM = 150, PULSE_MS = 60;
19const int WATER_LIMIT = 1200;            // 12 bit ölçekte, test ederek bul / on the 12-bit scale, find by testing
20const float IMPACT_G = 2.5, LYING_DEG = 70, PICKUP_DEG = 60;
21const unsigned long CANCEL_MS = 20000, SMS_REPEAT_MS = 300000;   // 5 dakikada bir konum / location every 5 minutes
22
23enum Mode { WALK, SETTLE, CHECK, ALARM, SOS };
24Mode mode = WALK;
25const int8_t SOS_PATTERN[] = {1, -1, 1, -1, 1, -3, 3, -1, 3, -1, 3, -3, 1, -1, 1, -1, 1, -7};
26const int SOS_LEN = sizeof(SOS_PATTERN) / sizeof(SOS_PATTERN[0]);
27const unsigned long UNIT_MS = 150;
28
29TinyGPSPlus gps;
30bool soundOn = true, wet = false, waterAlert = false, waterHigh = false, sosOn = false, smsDue = false;
31bool btnDown = false, longFired = false;
32int distanceCm = 999, vibNow = -1, sosStep = 0, histIdx = 0;
33int hist[3] = {999, 999, 999};
34float gTotal = 1.0, tilt = 45;
35unsigned long lastPing = 0, lastPulse = 0, lastMpu = 0, lastWater = 0, lastBat = 0, lastBeep = 0;
36unsigned long modeStart = 0, waterStart = 0, sosStepStart = 0, btnSince = 0, lastSms = 0;
37
38void setVib(int v) {
39  if (v != vibNow) { analogWrite(VIB, v); vibNow = v; }
40}
41
42void mpuWrite(byte reg, byte val) {
43  Wire.beginTransmission(MPU);
44  Wire.write(reg);
45  Wire.write(val);
46  Wire.endTransmission();
47}
48
49bool readMpu() {
50  Wire.beginTransmission(MPU);
51  Wire.write(0x3B);
52  if (Wire.endTransmission(false) != 0) return false;
53  if (Wire.requestFrom(MPU, (uint8_t)6) != 6) return false;
54  float a[3];
55  for (int i = 0; i < 3; i++) {
56    uint8_t hi = Wire.read();
57    uint8_t lo = Wire.read();
58    a[i] = (int16_t)(((uint16_t)hi << 8) | lo) / 4096.0;   // ±8 g
59  }
60  gTotal = sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]);
61  tilt = atan2(sqrt(a[1] * a[1] + a[2] * a[2]), fabs(a[0])) * 180.0 / PI;
62  return true;
63}
64
65int pingCm() {
66  digitalWrite(TRIG, LOW);  delayMicroseconds(2);
67  digitalWrite(TRIG, HIGH); delayMicroseconds(10);
68  digitalWrite(TRIG, LOW);
69  unsigned long t = pulseIn(ECHO, HIGH, 12000UL);
70  return t == 0 ? 999 : (int)(t / 58);
71}
72
73int median3(int a, int b, int c) {
74  if ((a <= b && b <= c) || (c <= b && b <= a)) return b;
75  if ((b <= a && a <= c) || (c <= a && a <= b)) return a;
76  return c;
77}
78
79int readButton(unsigned long now) {      // 0 yok, 1 kısa, 2 uzun / 0 none, 1 short, 2 long
80  bool pressed = digitalRead(BUTTON) == LOW;
81  if (pressed && !btnDown) { btnDown = true; longFired = false; btnSince = now; }
82  if (pressed && btnDown && !longFired && now - btnSince >= 2000) { longFired = true; return 2; }
83  if (!pressed && btnDown) {
84    btnDown = false;
85    if (!longFired && now - btnSince >= 30) return 1;
86  }
87  return 0;
88}
89
90void feedGps() {                          // NMEA cümlelerini çöz / parse the NMEA sentences
91  while (Serial2.available()) gps.encode(Serial2.read());
92}
93
94// SIM800L'den belirli bir yanıtı bekle / wait for a given reply from the SIM800L
95bool waitFor(const char* token, unsigned long timeoutMs) {
96  String buf;
97  unsigned long start = millis();
98  while (millis() - start < timeoutMs) {
99    while (Serial1.available()) {
100      buf += (char)Serial1.read();
101      if (buf.indexOf(token) >= 0) return true;
102      if (buf.length() > 200) buf.remove(0, 100);
103    }
104    feedGps();                            // beklerken GPS'i kaçırma / keep reading GPS while waiting
105  }
106  return false;
107}
108
109bool sendSms(const String& text) {
110  Serial1.println("AT");                  // ilk AT baud hızını eşler / the first AT syncs the baud rate
111  if (!waitFor("OK", 1000)) {
112    Serial1.println("AT");
113    if (!waitFor("OK", 1000)) return false;
114  }
115  Serial1.println("AT+CMGF=1");           // metin modu / text mode
116  if (!waitFor("OK", 1000)) return false;
117  Serial1.print("AT+CMGS=\"");
118  Serial1.print(PHONE);
119  Serial1.println("\"");
120  if (!waitFor(">", 5000)) return false;  // mesaj istemi / message prompt
121  Serial1.print(text);
122  Serial1.write(26);                      // Ctrl+Z: gönder / Ctrl+Z: send
123  return waitFor("+CMGS", 20000);         // şebeke onayı / network confirmation
124}
125
126String sosText() {                        // SMS'te Türkçe harf kullanma / no Turkish letters in the SMS
127  String s = "SOS! Akilli baston yardim cagrisi. ";
128  if (gps.location.isValid()) {
129    s += "Konum: https://maps.google.com/?q=";
130    s += String(gps.location.lat(), 6) + "," + String(gps.location.lng(), 6);
131    s += " (" + String(gps.location.age() / 1000) + " sn once, " + String(gps.satellites.value()) + " uydu)";
132  } else {
133    s += "GPS konumu henuz yok.";
134  }
135  return s;
136}
137
138void setMode(Mode m, unsigned long now) {
139  mode = m;
140  modeStart = now;
141  sosStep = 0;
142  sosStepStart = now;
143  sosOn = false;
144  setVib(0);
145  noTone(BUZZER);
146  if (m == SOS) { smsDue = true; lastSms = now; }
147}
148
149void fallUpdate(unsigned long now) {
150  if (mode == WALK && gTotal > IMPACT_G) setMode(SETTLE, now);
151  else if (mode == SETTLE && now - modeStart >= 1000) setMode(CHECK, now);
152  else if (mode == CHECK) {
153    if (tilt < LYING_DEG || fabs(gTotal - 1.0) > 0.2) setMode(WALK, now);
154    else if (now - modeStart >= 2000) setMode(ALARM, now);
155  } else if (mode == ALARM) {
156    if (tilt < PICKUP_DEG) setMode(WALK, now);
157    else if (now - modeStart >= CANCEL_MS) setMode(SOS, now);
158  }
159}
160
161void waterUpdate(unsigned long now) {
162  digitalWrite(WATER_PWR, HIGH);
163  delay(10);
164  int v = analogRead(WATER_SIG);          // 0–4095
165  digitalWrite(WATER_PWR, LOW);
166  if (v > WATER_LIMIT && !wet) { wet = true; waterAlert = true; waterStart = now; }
167  else if (v < WATER_LIMIT - 400) wet = false;
168}
169
170void walkOutput(unsigned long now) {
171  if (waterAlert && now - waterStart >= 1800) waterAlert = false;
172  if (waterAlert && now - lastBeep >= 300) {
173    lastBeep = now;
174    waterHigh = !waterHigh;
175    tone(BUZZER, waterHigh ? 1600 : 1000, 250);
176  }
177  if (distanceCm < 2 || distanceCm > FAR_CM) {
178    setVib(0);
179  } else if (distanceCm <= NEAR_CM) {
180    setVib(255);
181    if (soundOn && !waterAlert && now - lastBeep >= 120) { lastBeep = now; tone(BUZZER, 2500, 60); }
182  } else {
183    unsigned long period = map(distanceCm, NEAR_CM, FAR_CM, 120, 900);
184    int strength = map(distanceCm, NEAR_CM, FAR_CM, 255, 150);
185    if (now - lastPulse >= period) {
186      lastPulse = now;
187      if (soundOn && !waterAlert) tone(BUZZER, 2000, 30);
188    }
189    setVib(now - lastPulse < PULSE_MS ? strength : 0);
190  }
191}
192
193void sosOutput(unsigned long now) {
194  if (smsDue || now - lastSms >= SMS_REPEAT_MS) {      // girişte ve 5 dakikada bir / on entry and every 5 minutes
195    smsDue = false;
196    lastSms = now;
197    noTone(BUZZER);
198    bool ok = sendSms(sosText());
199    Serial.println(ok ? "SMS gonderildi / SMS sent" : "SMS gonderilemedi / SMS failed");
200    sosStepStart = millis();
201    sosOn = false;
202  }
203  if (now - sosStepStart >= (unsigned long)abs(SOS_PATTERN[sosStep]) * UNIT_MS) {
204    sosStep = (sosStep + 1) % SOS_LEN;
205    sosStepStart = now;
206  }
207  bool on = SOS_PATTERN[sosStep] > 0;
208  if (on != sosOn) {
209    sosOn = on;
210    if (on) tone(BUZZER, 3000); else noTone(BUZZER);
211    setVib(on ? 255 : 0);
212  }
213}
214
215void setup() {
216  Serial.begin(115200);
217  Serial2.begin(9600, SERIAL_8N1, GPS_RX, GPS_TX);    // NEO-6M varsayılanı 9600 baud / NEO-6M default is 9600 baud
218  Serial1.begin(9600, SERIAL_8N1, GSM_RX, GSM_TX);    // SIM800L
219  pinMode(BUTTON, INPUT_PULLUP);
220  pinMode(TRIG, OUTPUT);
221  pinMode(ECHO, INPUT);
222  pinMode(WATER_PWR, OUTPUT);
223  Wire.begin(21, 22);
224  mpuWrite(0x6B, 0x00);
225  mpuWrite(0x1A, 0x03);
226  mpuWrite(0x1C, 0x10);
227  tone(BUZZER, 1500, 100);
228}
229
230void loop() {
231  unsigned long now = millis();
232  feedGps();
233
234  int b = readButton(now);
235  if (b == 2) setMode(SOS, now);
236  else if (b == 1) {
237    if (mode == ALARM || mode == SOS) setMode(WALK, now);              // alarmı ya da SOS'u iptal et / cancel the alarm or SOS
238    else { soundOn = !soundOn; tone(BUZZER, soundOn ? 2000 : 600, 120); }   // yürürken: ses aç/kapat / while walking: sound on/off
239  }
240
241  if (now - lastMpu >= 20) {
242    lastMpu = now;
243    if (readMpu()) fallUpdate(now);
244  }
245
246  if (mode == WALK || mode == SETTLE || mode == CHECK) {
247    if (now - lastPing >= 60) {
248      lastPing = now;
249      hist[histIdx] = pingCm();
250      histIdx = (histIdx + 1) % 3;
251      distanceCm = median3(hist[0], hist[1], hist[2]);
252    }
253    if (now - lastWater >= 500) { lastWater = now; waterUpdate(now); }
254    walkOutput(now);
255  } else if (mode == ALARM) {
256    setVib((now - modeStart) % 1000 < 150 ? 255 : 0);
257    if (now - lastBeep >= 1000) { lastBeep = now; tone(BUZZER, 2000, 150); }
258  } else {
259    sosOutput(now);
260  }
261
262  if (now - lastBat >= 30000) {
263    lastBat = now;
264    float v = analogReadMilliVolts(BAT) * 2 / 1000.0;  // kalibreli mV okuma / calibrated mV reading
265    Serial.printf("Pil / Battery: %.2f V, GPS: %s\n", v, gps.location.isValid() ? "var / fix" : "yok / no fix");
266    if (v < 3.4 && mode == WALK) tone(BUZZER, 800, 400);
267  }
268}

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.

MicroPython
1from machine import Pin, PWM, I2C, ADC, time_pulse_us
2import time, math
3
4# Pinler: ECHO 1 kΩ / 2 kΩ bölücüyle / Pins: ECHO via a 1 kΩ / 2 kΩ divider
5trig, echo = Pin(2, Pin.OUT), Pin(3, Pin.IN)
6i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400000)    # MPU6050
7button = Pin(14, Pin.IN, Pin.PULL_UP)
8vib = PWM(Pin(15))
9vib.freq(1000)
10buzzer = PWM(Pin(16))
11water_pwr, water_sig = Pin(22, Pin.OUT), ADC(26)
12bat = ADC(28)                                         # 100k/100k bölücü / 100k/100k divider
13
14NEAR_CM, FAR_CM, PULSE_MS = 30, 150, 60
15WATER_LIMIT = 20000                                   # 16 bit ölçekte, test ederek bul / on the 16-bit scale, find by testing
16IMPACT_G, LYING_DEG, PICKUP_DEG, CANCEL_MS = 2.5, 70, 60, 20000
17SOS = [1, -1, 1, -1, 1, -3, 3, -1, 3, -1, 3, -3, 1, -1, 1, -1, 1, -7]   # Mors kodu / Morse code
18UNIT_MS = 150
19MPU = 0x68
20
21i2c.writeto_mem(MPU, 0x6B, b'\x00')                   # uyandır / wake up
22i2c.writeto_mem(MPU, 0x1A, b'\x03')                   # 44 Hz filtre / 44 Hz filter
23i2c.writeto_mem(MPU, 0x1C, b'\x10')                   # ±8 g
24
25beep_off = None
26
27def tone(freq, ms=0):                                 # ms = 0: noTone'a kadar çalar / plays until quiet()
28    global beep_off
29    buzzer.freq(freq)
30    buzzer.duty_u16(32768)                            # %50 doluluk / 50% duty
31    beep_off = time.ticks_add(time.ticks_ms(), ms) if ms else None
32
33def quiet():
34    global beep_off
35    buzzer.duty_u16(0)
36    beep_off = None
37
38def ping_cm():
39    trig.low(); time.sleep_us(2)
40    trig.high(); time.sleep_us(10)                    # 10 µs tetik / 10 µs trigger
41    trig.low()
42    t = time_pulse_us(echo, 1, 12000)                 # yankı yoksa negatif / negative if no echo
43    return 999 if t < 0 else t // 58
44
45def read_mpu():
46    d = i2c.readfrom_mem(MPU, 0x3B, 6)
47    a = []
48    for i in (0, 2, 4):
49        v = (d[i] << 8) | d[i + 1]
50        if v > 32767:
51            v -= 65536                                # işaretli 16 bit / signed 16-bit
52        a.append(v / 4096)                            # ±8 g: 4096 LSB/g
53    g = math.sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2])
54    tilt = math.atan2(math.sqrt(a[1] * a[1] + a[2] * a[2]), abs(a[0])) * 180 / math.pi
55    return g, tilt
56
57mode, mode_t = "walk", time.ticks_ms()
58sound_on, wet, water_alert, water_high = True, False, False, False
59hist, hist_i, dist = [999, 999, 999], 0, 999
60sos_i, sos_t, sos_on = 0, 0, None
61btn_down, long_fired, btn_t = False, False, 0
62last_ping = last_pulse = last_mpu = last_water = last_bat = last_beep = water_t = time.ticks_ms()
63
64def set_mode(m):
65    global mode, mode_t, sos_i, sos_t, sos_on
66    mode, mode_t = m, time.ticks_ms()
67    sos_i, sos_t, sos_on = 0, mode_t, None
68    vib.duty_u16(0)
69    quiet()
70
71def button_event(now):                                # 0 yok, 1 kısa, 2 uzun / 0 none, 1 short, 2 long
72    global btn_down, long_fired, btn_t
73    pressed = button.value() == 0
74    if pressed and not btn_down:
75        btn_down, long_fired, btn_t = True, False, now
76    if pressed and btn_down and not long_fired and time.ticks_diff(now, btn_t) >= 2000:
77        long_fired = True
78        return 2
79    if not pressed and btn_down:
80        btn_down = False
81        if not long_fired and time.ticks_diff(now, btn_t) >= 30:
82            return 1
83    return 0
84
85tone(1500, 100)                                       # açılış sesi / power-on beep
86while True:
87    now = time.ticks_ms()
88    if beep_off is not None and time.ticks_diff(now, beep_off) >= 0:
89        quiet()
90
91    b = button_event(now)
92    if b == 2:
93        set_mode("sos")                               # uzun basış: elle SOS / long press: manual SOS
94    elif b == 1:
95        if mode in ("alarm", "sos"):
96            set_mode("walk")                          # alarmı ya da SOS'u iptal et / cancel the alarm or SOS
97        else:                                         # yürürken: ses aç/kapat / while walking: sound on/off
98            sound_on = not sound_on
99            tone(2000 if sound_on else 600, 120)
100
101    if time.ticks_diff(now, last_mpu) >= 20:          # 50 Hz
102        last_mpu = now
103        g, tilt = read_mpu()
104        el = time.ticks_diff(now, mode_t)
105        if mode == "walk" and g > IMPACT_G:
106            set_mode("settle")
107        elif mode == "settle" and el >= 1000:
108            set_mode("check")
109        elif mode == "check":
110            if tilt < LYING_DEG or abs(g - 1) > 0.2:
111                set_mode("walk")                      # yalnızca bir vuruş / just a tap
112            elif el >= 2000:
113                set_mode("alarm")
114        elif mode == "alarm":
115            if tilt < PICKUP_DEG:
116                set_mode("walk")                      # baston kaldırıldı / cane picked up
117            elif el >= CANCEL_MS:
118                set_mode("sos")
119
120    if mode in ("walk", "settle", "check"):
121        if time.ticks_diff(now, last_ping) >= 60:
122            last_ping = now
123            hist[hist_i] = ping_cm()
124            hist_i = (hist_i + 1) % 3
125            dist = sorted(hist)[1]                    # 3 ölçümün ortancası / median of 3
126        if time.ticks_diff(now, last_water) >= 500:
127            last_water = now
128            water_pwr.on()
129            time.sleep_ms(10)
130            w = water_sig.read_u16()
131            water_pwr.off()
132            if w > WATER_LIMIT and not wet:
133                wet, water_alert, water_t = True, True, now
134            elif w < WATER_LIMIT - 5000:
135                wet = False
136        if water_alert and time.ticks_diff(now, water_t) >= 1800:
137            water_alert = False
138        if water_alert and time.ticks_diff(now, last_beep) >= 300:
139            last_beep = now
140            water_high = not water_high
141            tone(1600 if water_high else 1000, 250)   # iki tonlu su uyarısı / two-tone water alert
142        if dist < 2 or dist > FAR_CM:
143            vib.duty_u16(0)
144        elif dist <= NEAR_CM:
145            vib.duty_u16(65535)
146            if sound_on and not water_alert and time.ticks_diff(now, last_beep) >= 120:
147                last_beep = now
148                tone(2500, 60)
149        else:
150            period = 120 + (dist - NEAR_CM) * 780 // (FAR_CM - NEAR_CM)        # 120–900 ms
151            strength = 65535 - (dist - NEAR_CM) * 27000 // (FAR_CM - NEAR_CM)  # %100–%59
152            if time.ticks_diff(now, last_pulse) >= period:
153                last_pulse = now
154                if sound_on and not water_alert:
155                    tone(2000, 30)
156            vib.duty_u16(strength if time.ticks_diff(now, last_pulse) < PULSE_MS else 0)
157    elif mode == "alarm":
158        vib.duty_u16(65535 if time.ticks_diff(now, mode_t) % 1000 < 150 else 0)
159        if time.ticks_diff(now, last_beep) >= 1000:
160            last_beep = now
161            tone(2000, 150)                           # bulma sesi / locator beep
162    else:
163        if time.ticks_diff(now, sos_t) >= abs(SOS[sos_i]) * UNIT_MS:
164            sos_i = (sos_i + 1) % len(SOS)
165            sos_t = now
166        on = SOS[sos_i] > 0
167        if on != sos_on:
168            sos_on = on
169            if on:
170                tone(3000)
171            else:
172                quiet()
173            vib.duty_u16(65535 if on else 0)
174
175    if time.ticks_diff(now, last_bat) >= 30000:
176        last_bat = now
177        v = bat.read_u16() * 3.3 / 65535 * 2
178        print("Pil / Battery:", round(v, 2), "V")
179        if v < 3.4 and mode == "walk":
180            tone(800, 400)
181
182    time.sleep_ms(2)

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.

  1. Problema: Il bastone bianco non rileva gli ostacoli sopra la vita e le pozzanghere passano inosservate. Spiegalo con esempi concreti tratti dalle tue interviste.
  2. 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.
  3. 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.
  4. Risultati: Presenta i risultati delle prove qui sotto con tabelle e grafici e riporta anche i casi in cui ha fallito.
  5. 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.
ProvaCome si faValore da annotare
Precisione della distanza10 letture a 30, 60, 100 e 150 cm da un muroErrore medio (cm) e scostamento massimo
Tipi di ostacoloMuro, persona, palo da 5 cm, cespuglio, porta a vetri, tenda di stoffaQuante volte su 10 viene rilevato ogni ostacolo
Tempo di rispostaVideo al rallentatore con il telefono: comparsa dell'ostacolo e prima vibrazioneRitardo in millisecondi
Rilevamento dell'acquaImmergere la punta in 0,5, 1, 2 e 3 cm d'acquaDa quale profondità avvisa
Rilevamento cadute20 cadute e 100 colpi a terraNumero di allarmi corretti e falsi
AutonomiaFarlo funzionare da carica completa fino allo spegnimentoDurata in ore e tensione finale
Peso ed equilibrioPesare con una bilancia da cucina, misurare la distanza dall'impugnaturaPeso aggiunto (g) e coppia (kg·cm)
Fai le prove con utenti volontari bendati, in un corridoio vuoto e sicuro e sempre con un accompagnatore accanto; non fare mai prove vicino al traffico. Prepara un modulo di consenso per ogni prova con persone e controlla quali documenti etici richiede il regolamento del concorso. Molto probabilmente la giuria chiederà "vede le porte a vetri?", "cosa succede con la pioggia?" e "cosa succede quando la batteria si scarica?"; prepara le risposte in anticipo con i dati delle tue tabelle di prova.

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

SintomoCausa probabileSoluzione
Il bastone vibra senza sostaIl sensore punta al suolo o al bastone stessoAumenta l'angolo di montaggio e lascia libero lo spazio davanti al sensore
Vibrazioni sporadiche senza motivoUn eco errato isolato o un'altra sorgente di ultrasuoni vicinaIl filtro mediano c'è già; se serve, usa la mediana di 5 letture
A grandi distanze il motore non giraIl valore PWM è sotto la soglia di avvio del motoreTieni la forza minima sopra 150 e fissa il motore con la gomma
Arduino si riavvia quando parte il motoreLa corrente del motore fa crollare l'alimentazione o manca il diodoUsa un modulo pronto e aggiungi un condensatore da 100 µF sull'alimentazione
L'MPU6050 legge sempre 0 o un valore fissoSDA e SCL invertiti, indirizzo 0x69 o chip in sospensioneTrova l'indirizzo con uno scanner I2C e verifica che nel registro 0x6B venga scritto 0
Falsi allarmi battendo il bastone a terraIMPACT_G basso e soglia di inclinazione vicina all'angolo di camminoMisura l'inclinazione con il codice di prova e porta LYING_DEG a 75
L'avviso d'acqua non smette mai o non suona maiSoglia sbagliata o piste ossidateRimisura 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 attivoUsa un buzzer passivo per tone()
L'SMS non parteIl SIM800L non è registrato sulla rete, l'alimentazione è debole o il PIN della SIM è attivoControlla che il LED del modulo lampeggi ogni 3 secondi, disattiva il PIN e alimenta il modulo dalla 18650
Quando cerchi un guasto, prova i componenti uno alla volta: prima controlla con il codice di prova che distanza, valore dell'acqua e inclinazione abbiano senso, poi carica il codice completo. Se l'MPU6050 non risponde, lo scanner dell'articolo Che cos'è l'I2C ne trova l'indirizzo. Se la scheda si riavvia quando parte la vibrazione, la causa è quasi sempre l'alimentazione; l'articolo Arduino si riavvia da solo risolve questo problema passo dopo passo.

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.

Vedi tutti i progetti
HC-SR04 Misura della distanza (display seriale)
#27Principiante

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

4.6(46)
71
Uso del cicalino
#13Principiante

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

4.7(89)
161
Impostazione della frequenza con cicalino
#14Intermedio

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

4.6(69)
118
Sensore di parcheggio con HC-SR04
#28Intermedio
Premium

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

4.8(64)
85
ESP32 HC-SR04 Misuratore di distanza a ultrasuoni
#116Intermedio

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

4.9(28)
45
Sensore di parcheggio ESP32 (HC-SR04 + barra a 5 LED + cicalino + OLED)
#142Avanzato
Premium

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

0.0(0)
14
Robot che evita gli ostacoli
#81Avanzato
Premium

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.

5.0(38)
49
Sistema di Allarme Movimento ESP32 (PIR + Buzzer + LED + Telegram)
#145Avanzato
Premium

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

0.0(0)
9