Bastón inteligente Arduino para ciegos: obstáculos, agua y caídas
Cómo hacer un bastón inteligente para ciegos con Arduino: obstáculos con HC-SR04, aviso por vibración, sensor de agua, caídas con MPU6050, SOS y el código.
Un bastón inteligente para ciegos con Arduino avisa a una persona con discapacidad visual de los obstáculos que el bastón blanco no puede detectar, y es uno de los proyectos de electrónica más populares para trabajos escolares y ferias de ciencias. Un sensor ultrasónico HC-SR04 montado cerca del mango mide la distancia hasta lo que hay delante; a medida que el obstáculo se acerca, un motor de vibración en el mango pulsa más rápido y con más fuerza y, si quieres, un zumbador añade un pitido corto con cada pulso. Así la persona nota con varios pasos de antelación los obstáculos por encima de la cintura, que la punta del bastón nunca toca.
En esta guía empezarás con el circuito básico y añadirás, paso a paso, los extras que hacen que el proyecto sea realmente útil: un sensor de charcos cerca de la punta del bastón, un algoritmo que usa un MPU6050 para saber que el bastón se ha caído y emite un SOS si nadie lo cancela en 20 segundos, una alimentación que funciona más de 25 horas con una sola celda 18650, una caja diseñada pensando en el peso y la ergonomía, y una ampliación con ESP32 + GPS que envía la ubicación por SMS. Al final también encontrarás consejos concretos para probar el proyecto y presentarlo en una feria de ciencias.

¿Qué es un bastón inteligente y para qué sirve?
El bastón blanco es la herramienta en la que más confía una persona ciega: con cada toque revela el suelo, el bordillo, un escalón o un obstáculo bajo. Pero un bastón solo "ve" lo que toca su punta. Los obstáculos por encima de la cintura, como una rama baja, un cartel que sobresale sobre la acera, el retrovisor de un camión, una ventana abierta o un aparato de aire acondicionado en la fachada, nunca tocan la punta y golpean el cuerpo o la cabeza. El bastón inteligente está pensado para cubrir ese hueco: un sensor electrónico explora las alturas y distancias a las que el bastón no llega y transmite el resultado a la mano en forma de vibración.
Dejemos clara una cosa desde el principio: un bastón inteligente no sustituye al bastón blanco ni al entrenamiento en orientación y movilidad, los complementa. El sensor ultrasónico tiene puntos ciegos y las baterías se agotan, así que la regla de oro del diseño es que el bastón siga funcionando exactamente igual aunque la electrónica esté totalmente apagada. El Día Internacional del Bastón Blanco, que se celebra cada 15 de octubre, es una gran ocasión para presentar este proyecto en tu centro escolar.
¿Cómo funciona un bastón inteligente?
El proyecto es un único bucle que se repite cientos de veces por segundo y atiende cuatro tareas independientes a la vez. Cada 60 milisegundos el Arduino mide la distancia, toma la mediana de las tres últimas lecturas y ajusta el ritmo de la vibración. Cada medio segundo lee el sensor de agua de la punta. Cincuenta veces por segundo lee las aceleraciones del MPU6050 para comprobar si el bastón se ha caído. Y cada treinta segundos revisa la tensión de la batería. El diseño tiene dos canales de salida con funciones separadas: la vibración es solo para los obstáculos, mientras que el sonido indica situaciones especiales como agua, caída, SOS y batería baja. Si quieres, una pulsación corta del botón activa o desactiva los pitidos de obstáculos.
| Componente | Función | Nota |
|---|---|---|
| Arduino Nano (Uno para el prototipo) | Mide, decide, controla el motor y el zumbador | El Nano encaja en un bastón por ser pequeño y ligero |
| Sensor ultrasónico HC-SR04 | Mide la distancia al obstáculo de delante | 5 V, unos 15 mA; JSN-SR04T para lluvia |
| Módulo de motor de vibración | Avisa del obstáculo a la mano con vibración | Módulo listo con transistor y diodo |
| Zumbador pasivo | Avisos de agua, caída, SOS y batería | Pasivo, para poder tocar tonos distintos |
| Sensor de nivel de agua | Avisa cuando la punta entra en el agua | 1–2 cm por encima de la punta, alimentado solo al medir |
| MPU6050 (GY-521) | Detecta que el bastón se ha caído y está tumbado | I2C, dirección 0x68 |
| Pulsador | Sonido sí/no, cancelar alarma, SOS con pulsación larga | Colocado bajo el pulgar |
| Celda 18650 + TP4056 + MT3608 | Alimentación portátil, carga por USB, elevación a 5 V | Usa un TP4056 con protección (DW01) |
| 2 resistencias de 100 kΩ | Divisor para medir la tensión de la batería | El punto medio va a A0 |
| Interruptor y caja | Encendido/apagado y protección | Impresa en 3D o una cajita de proyectos |




Detección de obstáculos: ¿cómo mide el HC-SR04 y dónde se coloca?
Cuando el HC-SR04 recibe un pulso de 10 microsegundos en TRIG, emite ocho ondas de sonido a 40 kHz y mantiene ECHO en HIGH hasta que vuelve el eco. A 20 °C el sonido recorre unos 343 metros por segundo; contando ida y vuelta, pasan unos 58 microsegundos por cada centímetro. Por eso la distancia es simplemente cm = tiempo (µs) / 58. El sensor ve dentro de un cono de unos 15 grados; a 1,5 metros ese cono mide unos 40 cm de ancho. Como el sensor gira con el bastón al barrer de lado a lado, explora automáticamente una franja de alrededor de un metro delante de ti. Tienes los detalles del sensor en el artículo Sensor de distancia HC-SR04.
El detalle más importante del montaje es el ángulo. Al caminar, el bastón no va vertical, sino inclinado hacia delante unos 40–50 grados respecto a la vertical. Si pegas el sensor paralelo al bastón, apuntará al suelo y lo confundirá constantemente con un obstáculo. Fija la caja cerca del mango, a unos 75 cm del suelo, y orienta el sensor un poco por encima de la horizontal. Con el bastón inclinado 45 grados, el eje del sensor debe formar 60 grados con la caña (medidos hacia la punta) para que el sensor apunte 15 grados por encima de la horizontal. La forma más fácil de encontrar el ángulo correcto es sujetar el bastón como siempre y apoyar una app de nivel de burbuja del móvil en la cara frontal de la caja.
| Ángulo del sensor (sobre la horizontal) | Altura explorada a 1,5 m | Obstáculos típicos que detecta |
|---|---|---|
| 0° | Unos 55–95 cm | Bancos, papeleras, la carrocería de un coche aparcado |
| 15° | Unos 95–135 cm | El torso de una persona, el manillar de una bici, la caja de un camión |
| 25° | Unos 120–170 cm | Carteles, ramas, hojas de ventana abiertas, retrovisores |
Si empiezas con un solo sensor, 15–20 grados es un buen término medio: capta bien los obstáculos a la altura del cuerpo. Los obstáculos a la altura de la cabeza, en cambio, solo entran por el borde superior del cono mientras están lejos y salen de él al acercarte. Si la altura de la cabeza es importante para ti, dedícale un segundo HC-SR04 inclinado 30–35 grados hacia arriba; como este sensor mide en diagonal, mantén su alcance en unos 200 cm. Recuerda disparar los dos sensores por turnos y con al menos 60 ms entre ellos, o uno oirá el eco del otro. Para una comparación detallada con sensores láser (ToF) e infrarrojos, consulta el artículo comparativa de sensores de distancia.
| Sensor | Alcance | Ventajas | Inconvenientes |
|---|---|---|---|
| HC-SR04 | 2–400 cm | Barato, sencillo, cono amplio | No es impermeable, le cuestan las superficies blandas |
| JSN-SR04T | Unos 25–450 cm | Sonda impermeable, funciona con lluvia | No ve a menos de 25 cm, más caro |
| VL53L1X (láser ToF) | Hasta 4 m | Pequeño, ligero, haz definido | El alcance baja con luz solar, I2C a 3,3 V |
| Sharp GP2Y0A02 (infrarrojo) | 20–150 cm | Salida analógica, código sencillo | Se confunde con superficies oscuras y brillantes, consume unos 33 mA |
Ritmo de vibración y pitidos según la distancia
Una persona ciega percibe su entorno en gran parte a través del oído: el ruido de un coche que se acerca, el murmullo de la gente o el eco de los toques del bastón en una pared la orientan. Un zumbador que pita sin parar tapa esos sonidos más que ayudar. Por eso el canal principal de aviso de este proyecto es la vibración, y la regla es sencilla: cuanto más cerca está el obstáculo, más frecuentes y fuertes son los pulsos de vibración. Todo lo que está a más de 150 cm se ignora, y si hay un obstáculo a menos de 30 cm el motor vibra sin parar. Entre medias, el intervalo entre pulsos se calcula de forma lineal con map(): intervalo = 120 + (distancia − 30) × 6,5 ms.
¿Por qué 150 cm? Quien camina con bastón blanco avanza aproximadamente 1 metro por segundo. Un alcance de 150 cm le da cerca de 1,5 segundos antes de llegar al obstáculo; como el tiempo de reacción a un aviso táctil es de 0,2–0,3 segundos, basta para detenerse o cambiar de dirección. Si amplías el alcance a 250 cm, en una acera concurrida el bastón vibrará sin parar y al cabo de un tiempo el usuario dejará de hacer caso a los avisos. En interiores funcionan bien 100 cm y en exteriores 150–200 cm; solo tienes que cambiar la constante FAR_CM.
| Distancia | Intervalo entre pulsos | Fuerza de vibración (PWM) | Significado para el usuario |
|---|---|---|---|
| Más de 150 cm | Sin vibración | 0 | Camino libre |
| 150 cm | 900 ms | 150 | Hay un obstáculo más adelante |
| 100 cm | 575 ms | 194 | El obstáculo se acerca, atención |
| 60 cm | 315 ms | 229 | Frena y prepárate para cambiar de dirección |
| 40 cm | 185 ms | 247 | Muy cerca |
| 30 cm o menos | Continua | 255 | Para |

El motor más usado en los bastones es un motor ERM tipo moneda (coin), como el de los móviles. Funciona a unos 3 V y consume entre 60 y 80 mA. Un pin del Arduino, en cambio, da como máximo 40 mA y el límite seguro es de 20 mA, así que el motor no se conecta directamente al pin sino a través de un transistor. Los módulos listos para usar ya incluyen ese transistor y el diodo que absorbe la tensión inversa cuando el motor se detiene; la mayoría funciona con 3,3–5 V. Si usas un motor suelto, basta con un transistor NPN como el BC337 o el 2N2222, una resistencia de base de 1 kΩ y un diodo 1N4148 en antiparalelo con el motor; puedes comparar transistores en la herramienta Transistores.
La posición del motor importa tanto como el código. Colócalo dentro del mango, donde apoyan la palma o el índice, y fíjalo con goma fina en lugar de espuma, porque la espuma absorbe la vibración. A plena velocidad, un motor tipo moneda vibra a unos 200 Hz, y nuestra piel es más sensible a la vibración en torno a 200–300 Hz, así que incluso un motor pequeño en el sitio correcto se nota con claridad. Como los motores ERM no arrancan con valores PWM muy bajos, la vibración más débil del código se mantiene en 150 sobre 255. En el artículo motor de vibración encontrarás más sobre la elección del motor y el control de la intensidad con PWM.
Esquema de conexiones: Arduino Uno/Nano, ESP32 y Raspberry Pi Pico
La tabla siguiente coincide exactamente con todos los códigos del artículo. En el Uno y el Nano los números de pin son los mismos; en ambas placas el bus I2C del MPU6050 está en A4 (SDA) y A5 (SCL). El VCC del módulo de vibración va a 5V, y su pin IN se puso a propósito en D5: tone() usa el Timer2, así que mientras suena el zumbador el PWM de D3 y D11 deja de funcionar, mientras que D5 pertenece al Timer0 y no se ve afectado. Puedes ver el mapa de pines del Nano en la herramienta Pinout.
| Conexión | Arduino Uno / Nano | ESP32 | Raspberry Pi Pico |
|---|---|---|---|
| HC-SR04 VCC | 5V | VIN (5 V) | VBUS o línea de 5 V |
| HC-SR04 TRIG | D9 | GPIO 18 | GP2 |
| HC-SR04 ECHO | D10 (directo) | GPIO 19, con divisor 1 kΩ / 2 kΩ | GP3, con divisor 1 kΩ / 2 kΩ |
| Módulo de vibración IN | D5 (PWM) | GPIO 25 | GP15 |
| Zumbador pasivo (+) | D8 | GPIO 26 | GP16 |
| Pulsador (otra pata a GND) | D2 | GPIO 27 | GP14 |
| Sensor de agua + | D7 | GPIO 33 | GP22 |
| Sensor de agua S | A1 | GPIO 34 (ADC1) | GP26 (ADC0) |
| MPU6050 SDA | A4 | GPIO 21 | GP4 |
| MPU6050 SCL | A5 | GPIO 22 | GP5 |
| MPU6050 VCC | 5V | 3V3 | 3V3 |
| Medida de batería (punto medio del divisor 100k/100k) | A0 | GPIO 35 (ADC1) | GP28 (ADC2) |
| GPS NEO-6M TX / RX | No | GPIO 16 / GPIO 17 | No |
| SIM800L TXD / RXD | No | GPIO 13 / GPIO 23 | No |
Montaje paso a paso: primero el bastón básico
- Monta primero el circuito en una protoboard y deja la instalación en el bastón para el final. Coloca el Arduino Nano en el centro de la protoboard y lleva 5V y GND a los raíles laterales.
- Conecta el VCC y el GND del HC-SR04 a los raíles, TRIG a D9 y ECHO a D10.
- Conecta el VCC del módulo de vibración a 5V, su GND a GND y su pin IN (S en algunos módulos) a D5. Conecta la pata positiva del zumbador pasivo a D8 y la otra a GND.
- Sube el código básico de abajo y abre el monitor serie a 9600 baudios. Acerca y aleja la mano del sensor despacio; la distancia en pantalla debería cambiar y la vibración acelerarse y frenarse con ella.
- Coloca el sensor a 150, 100, 60 y 30 cm de una pared y compara el valor del monitor serie con un metro. Si la diferencia supera 1–2 cm, revisa el cableado y la alimentación.
- Cuando todo funcione, añade el sensor de agua, el MPU6050 y el pulsador y pasa al código 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}En el código no hay ningún delay() largo; cada tarea funciona con marcas de tiempo tomadas con millis(), así que los pulsos de vibración nunca retrasan las mediciones. Para pulseIn() se eligió un tiempo de espera de 12 ms: como 12 000 / 58 ≈ 200 cm, los ecos más lejanos no nos sirven de nada y un tiempo de espera corto evita que el bucle se atasque. Si no vuelve ningún eco, la función devuelve 999, que se interpreta como "camino libre". setVib() solo escribe en el motor cuando cambia el valor; como en las versiones 2.x del núcleo del ESP32 analogWrite() reconfigura la unidad PWM en cada llamada, este pequeño detalle hace que el mismo código funcione sin problemas también allí.
Sensor de charcos: aviso cuando la punta del bastón entra en el agua
Los charcos son de los obstáculos más difíciles de notar con el bastón blanco: cuando la punta entra en el agua, el sonido y la sensación apenas se distinguen del suelo, pero unos centímetros de agua bastan para empaparte los zapatos. Para esto se usa un sensor de nivel de agua barato con pistas de cobre entrelazadas. Unas pistas van a la alimentación y otras a la línea de señal; cuando el agua las une, la tensión de la señal sube y el valor de analogRead() aumenta. En seco el sensor lee cerca de 0, y con la punta en el agua suele dar unos cuantos cientos en el Uno; cuanto más se sumerge, mayor es el valor.
Coloca el sensor en la parte más baja del bastón, con el borde inferior a unos 1–2 cm de la punta y las pistas perpendiculares al suelo. Así, en suelo seco nada lo toca, pero en un charco de más de 1–2 cm las pistas se mojan. El punto débil conocido de estos sensores es que las pistas de cobre se corroen rápidamente por electrólisis si están alimentadas todo el tiempo. La solución del código es conectar el pin + del sensor a D7 en lugar de directamente a 5V y alimentarlo solo durante los 10 ms de la medición. Con una lectura cada medio segundo, el sensor solo está con tensión el 2 % del tiempo. Tienes su funcionamiento en el artículo sensor de nivel de agua y su diferencia con la placa de lluvia en el artículo sensor de lluvia.

Detección de caídas con el MPU6050
El MPU6050 es un sensor de movimiento con acelerómetro de tres ejes y giroscopio de tres ejes en un mismo chip, conectado por I2C. La placa GY-521 lleva regulador, así que puede alimentarse a 5 V; su dirección por defecto es 0x68. En este proyecto solo usamos el acelerómetro y, en lugar de una librería, hablamos directamente con cuatro registros: escribir 0 en 0x6B despierta el chip, 0x1A activa el filtro interno de 44 Hz, escribir 0x10 en 0x1C amplía el rango a ±8 g, y leemos las aceleraciones X, Y y Z de los seis bytes que empiezan en 0x3B. El rango por defecto de ±2 g se satura fácilmente en el momento en que el bastón golpea el suelo y oculta la magnitud real del impacto. En el rango de ±8 g, cada 1 g equivale a 4096 cuentas brutas. Para conocer mejor el sensor, echa un vistazo al artículo acelerómetro y giroscopio MPU6050.

En cada lectura el código calcula dos magnitudes. La primera es la aceleración total: g = √(ax² + ay² + az²). Con el bastón quieto en la mano o tumbado en el suelo vale alrededor de 1 g; en un golpe fuerte sube a varios g. La segunda es la inclinación, el ángulo entre el bastón y la vertical. Si montas la placa con la flecha X a lo largo de la caña, la inclinación es atan2(√(ay² + az²), |ax|): marca 0 grados con el bastón vertical, 40–55 al caminar y 85–90 tumbado en el suelo. Los sistemas que detectan caídas de personas buscan primero una breve caída libre (un instante cerca de 0 g); pero un bastón que se vuelca se mueve como una varilla que gira alrededor de su punta, así que un sensor cerca del mango a menudo no ve ese instante. Por eso nuestro algoritmo no busca la caída libre, sino un impacto seguido de una posición horizontal e inmóvil.
| Fase | Condición | Duración | ¿Qué pasa? |
|---|---|---|---|
| WALK (marcha) | La aceleración total supera 2,5 g | Sin límite | Pasa a SETTLE; los avisos de obstáculos siguen |
| SETTLE (reposo) | Espera a que acabe la sacudida | 1 s | Pasa a CHECK |
| CHECK (comprobación) | La inclinación debe seguir por encima de 70° y la aceleración entre 0,8 y 1,2 g | 2 s | Si falla la condición vuelve a WALK; si se cumple, ALARM |
| ALARM | Un pitido y una vibración cada segundo para encontrar el bastón | 20 s | Se cancela con el pulsador o al levantar el bastón (inclinación menor de 60°) |
| SOS | ... --- ... en código Morse con sonido, LED y vibración | Hasta que se cancele | Una pulsación corta lo detiene; el ESP32 envía un SMS |
Esta secuencia evita la mayoría de las falsas alarmas. Golpear el suelo con el bastón al caminar también puede generar impactos breves de más de 2,5 g, pero un segundo después el bastón sigue en la mano e inclinado 40–55 grados, así que la fase de comprobación se cancela al instante; el usuario ni lo nota. En cambio, si el bastón cae y se queda tumbado, la alarma empieza a los 3 segundos aproximadamente, y el pitido de cada segundo permite a quien lo ha dejado caer encontrarlo por el sonido. Si el usuario recoge el bastón, la alarma se calla sola. Si nadie reacciona en 20 segundos, el código supone que puede tratarse de una caída real y empieza el SOS. Mantener pulsado el botón 2 segundos también inicia un SOS manual en cualquier momento.
Código de prueba y calibración
Los umbrales cambian un poco con cada bastón, cada suelo y cada sensor. Antes de pasar al código principal, sube el programa de abajo: cada cuarto de segundo muestra en el monitor serie la distancia, el valor bruto del sensor de agua, la aceleración máxima del último cuarto de segundo y la inclinación. Anotar los valores en una tabla también te rellena el apartado de resultados del informe del proyecto.
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}- Mide el sensor de agua en seco y con la punta sumergida 1 cm en un vaso de agua. Si, por ejemplo, lee 0–20 en seco y 520 mojado, elige el punto medio de 270–300 para WATER_LIMIT.
- Sujeta el bastón como siempre y bárrelo de lado a lado como si caminaras. Anota la inclinación; suele quedarse entre 40 y 55 grados y debe estar por debajo de PICKUP_DEG (60).
- Deja el bastón tumbado en el suelo. La inclinación debería marcar 85–90 y la aceleración unos 0,95–1,05 g, lo que confirma LYING_DEG (70) y la ventana de 0,8–1,2 g.
- Golpea el suelo con el bastón como siempre al caminar y observa los picos de g. Después deja caer el bastón 10 veces sobre una colchoneta. Ajusta IMPACT_G por debajo de los valores de caída; aunque los golpes superen ese umbral, la comprobación de inclinación los descarta.
- Toma 10 lecturas a 30, 60, 100 y 150 cm de una pared y compara la media con un metro.
Código completo: obstáculos, agua, caídas, SOS y aviso de batería
En el código completo las cuatro tareas funcionan en el mismo bucle sin bloquearse entre sí, y la detección de caídas es una pequeña máquina de estados hecha con enum y switch-case. El pulsador hace tres cosas: al caminar, una pulsación corta activa o desactiva los pitidos de obstáculos (la vibración siempre sigue activa), durante una alarma o un SOS una pulsación corta lo cancela, y una pulsación larga de 2 segundos inicia un SOS en cualquier momento. Un pitido agudo indica sonido activado y uno grave, desactivado.
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}| Función o ajuste | ¿Qué hace? |
|---|---|
| pingCm() | Hace una medición; devuelve 999 si no llega eco en 12 ms |
| median3() | Calcula la mediana de las tres últimas lecturas y descarta un valor erróneo aislado |
| readMpu() | Lee los seis bytes de aceleración y calcula la aceleración total y la inclinación |
| fallUpdate() | Ejecuta la máquina de estados WALK → SETTLE → CHECK → ALARM → SOS |
| waterUpdate() | Alimenta el sensor de agua 10 ms, lo lee y lanza el aviso al pasar de seco a mojado |
| walkOutput() | Genera la vibración según la distancia y el aviso de agua con un sonido de dos tonos |
| sosOutput() | Reproduce el array SOS_PATTERN paso a paso |
| readButton() | Distingue pulsaciones cortas (1) y largas (2) e ignora rebotes de menos de 30 ms |
| IMPACT_G, LYING_DEG, CANCEL_MS | Sensibilidad de caída y tiempo para cancelar; se ajustan con el código de prueba |
El patrón de SOS sigue las reglas del código Morse: el punto dura 1 unidad, la raya 3, con 1 unidad entre señales, 3 entre letras y 7 entre repeticiones. Con una unidad de 150 ms, un ciclo de SOS dura 34 unidades, unos 5,1 segundos. El tono de 3 kHz se eligió a propósito: el oído humano es más sensible entre 2 y 4 kHz, y los zumbadores pasivos pequeños también suenan más fuerte en ese rango. Para la diferencia entre zumbador activo y pasivo, consulta el artículo ¿Qué es un zumbador?.
Batería, alimentación y autonomía
Un bastón inteligente se usa al aire libre todo el día, así que el diseño de la alimentación importa tanto como los sensores. Calculemos el consumo medio: el Nano gasta unos 20 mA, el HC-SR04 15 mA y el MPU6050 4 mA. El motor de vibración consume unos 70 mA, pero solo durante los pulsos; en un paseo típico su media ronda los 10 mA. El zumbador y el sensor de agua añaden unos pocos mA. En total, unos 50 mA, es decir, 0,25 W a 5 V. Una celda 18650 de 2600 mAh almacena unos 9,6 Wh a 3,7 V; con un elevador del 85 % de rendimiento llegan al circuito unos 8 Wh. 8 / 0,25 = 32 horas; teniendo en cuenta que no se puede aprovechar toda la batería, una cifra realista es de 25–30 horas. Quien lo use dos horas al día lo cargará aproximadamente cada dos semanas. Puedes hacer tu propio cálculo con la herramienta Duración de batería y leer la diferencia entre mAh y Wh en el artículo mAh y Wh.
| Fuente de alimentación | Peso | Autonomía aprox. (50 mA) | Valoración |
|---|---|---|---|
| Pila de 9 V (unos 500 mAh), a VIN | 45 g | 8–10 horas | No recomendada: la tensión cae cuando arranca el motor y no es recargable |
| 4 pilas AA NiMH (2000 mAh), al pin 5V | Unos 120 g | 35–40 horas | Pesada; recién cargada llega a 5,6 V y fuerza el límite |
| 1 celda 18650 + TP4056 + MT3608 | Unos 55 g | 25–30 horas | Recomendada: ligera, carga por USB, protegida |
| 2 celdas 18650 en serie (7,4 V), a VIN | Unos 105 g | 45–50 horas | Mucha autonomía pero pesada, necesita su propio circuito de carga |
| Powerbank (10 000 mAh) | 200 g o más | Varios días | Pesada; la mayoría de powerbanks se apagan con poca corriente |

La cadena de alimentación recomendada es esta: la celda 18650 se conecta a los terminales B+ y B− del módulo TP4056. La línea que sale de OUT+ pasa por un interruptor deslizante y llega a la entrada del módulo elevador MT3608. Su salida, ajustada a 5,0 V, alimenta el pin 5V del Nano, el HC-SR04 y el módulo de vibración. Para medir la tensión de la batería se monta, justo después del interruptor, un divisor de dos resistencias de 100 kΩ con el punto medio en A0; como está después del interruptor, no descarga la batería con el bastón apagado. El chip DW01 desconecta la celda hacia los 2,4–2,5 V, pero el código avisa mucho antes con un pitido grave cada 30 segundos por debajo de 3,4 V. Más detalles en el artículo módulo de carga TP4056.
Caja, peso y ergonomía
La mayoría de los bastones blancos plegables pesan 200–300 g, y el usuario los barre de lado a lado con la muñeca durante horas. Cada gramo que añades se multiplica por su distancia a la mano y carga la muñeca. Si fijas una caja de 150 g 10 cm por debajo del mango, el par extra en la muñeca es 150 × 10 = 1,5 kg·cm; la misma caja 60 cm más abajo daría 9 kg·cm, seis veces más. Por eso reúne toda la electrónica en una sola caja justo debajo del mango; solo el sensor de agua y su cable fino bajan hasta la punta. El objetivo es que el peso añadido total no pase de 150 g.
| Componente | Peso aprox. |
|---|---|
| Arduino Nano | 7 g |
| HC-SR04 | 9 g |
| MPU6050 (GY-521) | 2 g |
| Módulo de vibración + zumbador pasivo | 5 g |
| Sensor de nivel de agua y cable | 8 g |
| Celda 18650 | 45–48 g |
| TP4056 + MT3608 + interruptor | 8 g |
| Caja impresa en 3D y abrazaderas | 40–50 g |
| Cables, tornillos, cinta de velcro | 10–15 g |
| Total | Unos 135–150 g |
- Material: Imprime la caja en PETG o ABS; el PLA puede ablandarse y deformarse dentro de un coche al sol. Basta con 1,6–2 mm de grosor de pared.
- Fijación: Sujeta la caja al bastón con dos abrazaderas o una cinta de velcro forrada de goma. En los bastones plegables la caja debe quedar por encima de la primera articulación, o el bastón no se podrá plegar.
- Ventana del sensor: Los dos cilindros del HC-SR04 deben sobresalir por la cara frontal de la caja; no pongas plástico fino ni rejilla delante, porque el ultrasonido no los atraviesa.
- Lluvia: Haz un pequeño agujero de drenaje en la parte inferior de la caja y mete un sobre de gel de sílice. Si vives en una zona muy lluviosa, usa la sonda impermeable JSN-SR04T en lugar del HC-SR04.
- Pulsador: Elige un pulsador grande con un clic claro y colócalo donde el pulgar descansa de forma natural al sujetar el mango. Añade un reborde de 2 mm alrededor para que no se pulse sin querer.
- Encendido: La posición del interruptor debe reconocerse al tacto; el pitido de encendido también confirma que el aparato funciona.
- Conector de carga: El conector USB del TP4056 debe ser accesible desde la parte inferior de la caja y taparse con un tapón de goma cuando no se use.
- Visibilidad: Una cinta reflectante en los laterales de la caja ayuda a que los conductores vean el bastón cuando el usuario cruza la calle al anochecer.
Envío de la ubicación con ESP32 + GPS
Con un Arduino, el bastón solo puede alertar con sonido a quien esté cerca durante un SOS. Con un ESP32 y un módulo GPS, en cambio, la ubicación del bastón puede enviarse como mensaje a un familiar. Por defecto, el módulo NEO-6M emite frases NMEA a 9600 baudios con una posición por segundo; la librería TinyGPSPlus las interpreta y te da latitud, longitud y número de satélites. Según la hoja de datos, la primera posición tras un arranque en frío tarda unos 27 segundos; en la práctica, al aire libre lleva entre 30 segundos y unos minutos, y en interiores a menudo no hay posición. La pequeña batería de respaldo del módulo guarda los últimos datos de satélites, así que en los arranques siguientes la posición llega en pocos segundos. Detalles en el artículo módulo GPS NEO-6M.

Hay dos formas de enviar el mensaje. Para una demostración en clase, lo más fácil es conectar el ESP32 al punto de acceso personal del móvil del usuario y enviar el mensaje con un bot de Telegram. Para un bastón que funcione de verdad en la calle, encaja mejor un módulo GSM SIM800L, que no depende del móvil: envía un SMS directamente con la tarjeta SIM que lleva dentro. El SIM800L necesita 3,4–4,4 V y tiene picos de corriente de hasta 2 A al transmitir, así que aliméntalo directamente desde la celda 18650 y no desde el elevador, y pon un condensador de 1000 µF en sus pines de alimentación. Este módulo solo funciona en redes 2G; en los países donde se ha apagado el 2G necesitarás un módulo 4G como el A7670 o el SIM7600. Si quieres tenerlo todo en una placa, puedes usar 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}El código usa la misma máquina de estados que la versión de Arduino; la diferencia está en el SOS. En cuanto empieza el SOS, sendSms() envía al SIM800L los comandos AT, AT+CMGF=1 para el modo texto y AT+CMGS="número", espera el indicador ">", escribe el mensaje y lo envía con Ctrl+Z (26). La ubicación aparece en el mensaje como un enlace de mapa que, al tocarlo en el móvil, abre la app de mapas. Si el GPS aún no tiene posición, el mensaje lo indica, y mientras dure el SOS la ubicación se reenvía cada 5 minutos. El texto del SMS evita a propósito las letras turcas: caracteres como ş, ğ e ı no están en el alfabeto GSM estándar y se ven mal en algunos móviles. Al enviar, el código puede esperar hasta 30 segundos; además, tras encenderse, el SIM800L tarda 10–30 segundos en registrarse en la red.
Bastón inteligente con la Raspberry Pi Pico (MicroPython)
En la Raspberry Pi Pico la misma lógica se programa en MicroPython. time_pulse_us() funciona como pulseIn() en Arduino y devuelve un valor negativo si no llega eco. Las salidas PWM se ajustan con duty_u16(), de 16 bits; en el motor de vibración, 65535 significa potencia máxima. Los seis bytes del MPU6050 se leen de una vez con i2c.readfrom_mem() y se convierten en números con signo restando 65536 a los valores mayores que 32767. Los pines de la Pico también son de 3,3 V, así que aquí el divisor de ECHO es igual de imprescindible. Con batería, lleva la salida de 5 V del MT3608 al pin VSYS; el HC-SR04 se alimenta de esa misma línea de 5 V. Con la Pico W, que tiene Wi-Fi, también puedes mostrar el estado del bastón en una página 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)Consejos para presentarlo en una feria de ciencias o un concurso
El bastón inteligente es un proyecto habitual en ferias de ciencias y concursos escolares (tienes más ideas en nuestro artículo de proyectos para ferias de ciencias), así que lo que impresiona al jurado no es que el bastón vibre, sino lo bien que defines el problema y la honestidad con la que mides tu solución. Los mejores proyectos empiezan con una conversación con una persona ciega o con una asociación: escuchar de ellos qué obstáculos son de verdad un problema, si prefieren vibración o sonido y cuánto puede pesar como máximo el bastón da a tu proyecto rumbo y una historia potente.
- Problema: El bastón blanco no detecta los obstáculos por encima de la cintura y los charcos pasan desapercibidos. Explícalo con ejemplos concretos sacados de tus entrevistas.
- Objetivo: Diseñar un accesorio que avise por vibración, con al menos 1 segundo de antelación, de los obstáculos a la altura del cuerpo y de la cabeza, sin cambiar demasiado el peso ni el uso del bastón.
- Método: Muestra la elección del sensor (HC-SR04, JSN-SR04T, ToF) con sus motivos, el ángulo de montaje con su cálculo y la relación de vibración con su fórmula.
- Resultados: Presenta los resultados de las pruebas de abajo en tablas y gráficos, e incluye los casos en que falló.
- Conclusiones y propuestas: Explica con claridad qué resuelve el proyecto, sus límites (cristal, superficies blandas, lluvia, detectar la caída del bastón y no la de la persona) y cuál sería el siguiente paso.
| Prueba | Cómo se hace | Valor que anotar |
|---|---|---|
| Precisión de distancia | 10 lecturas a 30, 60, 100 y 150 cm de una pared | Error medio (cm) y desviación máxima |
| Tipos de obstáculo | Pared, persona, poste de 5 cm, arbusto, puerta de cristal, cortina de tela | Cuántas veces de 10 se detecta cada obstáculo |
| Tiempo de respuesta | Vídeo a cámara lenta con el móvil: aparición del obstáculo y primera vibración | Retraso en milisegundos |
| Detección de agua | Sumergir la punta 0,5, 1, 2 y 3 cm en agua | A partir de qué profundidad avisa |
| Detección de caídas | 20 caídas y 100 golpes contra el suelo | Número de alarmas correctas y falsas |
| Autonomía | Funcionar desde carga completa hasta que se apague | Tiempo en horas y tensión final |
| Peso y equilibrio | Pesar con una báscula de cocina, medir la distancia al mango | Peso añadido (g) y par (kg·cm) |
Ideas para mejorarlo
- Aviso de escalones y agujeros: Un segundo HC-SR04 que apunte hacia abajo y hacia delante ve normalmente el suelo a una distancia constante; si la lectura se alarga de golpe, hay un escalón de bajada o un agujero delante.
- Mensajes de voz: Con un DFPlayer Mini puedes reproducir en un auricular frases grabadas como "obstáculo delante", "agua" o "batería baja" en lugar de pitidos.
- App en el móvil: Usa el Bluetooth BLE del ESP32 para mostrar la batería y los ajustes en el móvil y cambiar el alcance desde allí.
- Dirección: Pon dos motores, a la izquierda y a la derecha del mango, y usa la vibración para indicar en qué lado está el obstáculo.
- Modo reposo: Apaga los sensores cuando el MPU6050 detecte que el bastón lleva minutos sin moverse; con el deep sleep del ESP32 la autonomía se multiplica.
- Visibilidad nocturna: Con una LDR, haz parpadear un LED en la punta del bastón al anochecer para que los conductores vean al usuario desde lejos.
Errores frecuentes y soluciones
| Síntoma | Causa probable | Solución |
|---|---|---|
| El bastón vibra sin parar | El sensor apunta al suelo o al propio bastón | Sube el ángulo de montaje y deja libre el espacio delante del sensor |
| Vibraciones sueltas sin motivo | Algún eco erróneo aislado u otra fuente ultrasónica cercana | El filtro de mediana ya está; si hace falta, usa la mediana de 5 lecturas |
| El motor no gira a distancias largas | El valor PWM está por debajo del umbral de arranque del motor | Mantén la fuerza mínima por encima de 150 y fija el motor con goma |
| El Arduino se reinicia cuando arranca el motor | La corriente del motor hunde la alimentación o falta el diodo | Usa un módulo listo y añade un condensador de 100 µF a la alimentación |
| El MPU6050 siempre lee 0 o un valor fijo | SDA y SCL cambiados, dirección 0x69 o chip dormido | Busca la dirección con un escáner I2C y comprueba que se escribe 0 en el registro 0x6B |
| Falsas alarmas al golpear el suelo | IMPACT_G bajo y umbral de inclinación cercano al ángulo de marcha | Mide la inclinación con el código de prueba y sube LYING_DEG a 75 |
| El aviso de agua no se calla nunca o nunca suena | Umbral incorrecto o pistas oxidadas | Vuelve a medir los valores en seco y mojado y cambia el sensor si hace falta |
| El zumbador no suena o siempre da el mismo tono | Está montado un zumbador activo | Usa un zumbador pasivo para tone() |
| El SMS no se envía | El SIM800L no está registrado en la red, la alimentación es débil o el PIN de la SIM está activado | Comprueba que el LED del módulo parpadea cada 3 segundos, desactiva el PIN y alimenta el módulo desde la 18650 |
Un bastón inteligente reúne en un solo proyecto la medición de distancias, la respuesta háptica, la detección de eventos con un sensor de movimiento y el diseño de la alimentación, y como responde a una necesidad real, es uno de los proyectos de Arduino con más sentido que puedes hacer. Empieza con el código básico de esta guía, rellena tus tablas de pruebas y luego añade una a una las capas de agua, caídas y GPS. En la app BlueGrays encontrarás, paso a paso y con esquemas y código listo, proyectos de medición de distancia con el HC-SR04, un sensor de aparcamiento, sonido con zumbador y notificaciones con el ESP32, para que pruebes primero cada parte de tu bastón por separado.
Preguntas frecuentes
¿Cómo funciona un bastón inteligente para ciegos?
Un sensor ultrasónico HC-SR04 montado cerca del mango emite ondas de sonido y calcula la distancia al obstáculo a partir del tiempo que tarda el eco en volver. Según esa distancia, el Arduino acciona un motor de vibración en el mango: cuanto más cerca está el obstáculo, más rápida y fuerte es la vibración. En las versiones avanzadas, un sensor de agua en la punta detecta charcos y un MPU6050 detecta que el bastón se ha caído.
¿Qué materiales necesito para un bastón inteligente con Arduino?
Para la versión básica bastan un Arduino Nano, un sensor ultrasónico HC-SR04, un módulo de motor de vibración, un zumbador pasivo y cables de conexión. La versión completa añade un sensor de nivel de agua, un MPU6050, un pulsador y una celda 18650 con los módulos de carga TP4056 y elevador MT3608. Para enviar la ubicación necesitas un ESP32, un GPS NEO-6M y un módulo GSM SIM800L.
¿A qué distancia detecta obstáculos un bastón inteligente?
El HC-SR04 mide de 2 a 400 cm, pero en un bastón se suele usar un límite de 150 cm. Para alguien que camina a 1 metro por segundo, eso supone un aviso de 1,5 segundos antes de llegar al obstáculo. Un alcance mucho mayor provoca vibración constante en lugares concurridos y hace que el usuario ignore los avisos.
¿Cómo sabe un bastón inteligente que se ha caído?
El acelerómetro MPU6050 se lee 50 veces por segundo. Si tras un impacto de más de 2,5 g el bastón se queda horizontal e inmóvil durante 2 segundos, se considera que se ha caído y empieza un pitido para localizarlo. Si en 20 segundos nadie lo levanta ni pulsa el botón, suena el SOS; la versión con ESP32 envía en ese momento la ubicación por SMS.
¿Cuánto dura la batería de un bastón inteligente con Arduino?
El circuito consume de media unos 50 mA. Una sola celda 18650 de 2600 mAh con los módulos TP4056 y MT3608 da unas 25–30 horas, es decir, unas dos semanas usándolo dos horas al día. Una pila de 9 V solo dura 8–10 horas y no se recomienda porque su tensión cae cuando arranca el motor.
¿Se puede añadir GPS a un bastón inteligente para enviar la ubicación?
Sí. Si usas un ESP32 en lugar de un Arduino y añades un módulo GPS NEO-6M, puedes leer la latitud y la longitud del bastón. Con un módulo GSM SIM800L, la ubicación llega por SMS a un familiar como enlace de mapa durante un SOS; si hay Wi-Fi, también sirve un bot de Telegram. El GPS funciona al aire libre, pero en interiores normalmente no obtiene posición.
Proyectos para hacer después de leer esto
Ver todos los proyectos
HC-SR04 Medición de distancia (pantalla en serie)
El sensor HC-SR04 funciona igual que un murciélago que se orienta en la oscuridad: emite un sonido que no puedes oír, escucha el eco y mide a qué distancia está el objeto que tiene delante. El Arduino

Uso del zumbador
¿Alguna vez te has preguntado cómo suena un timbre o una alarma? El Arduino hace pitar un pequeño zumbador a intervalos cortos: pip, silencio, pip, silencio. Ves cómo se crea sonido con electricidad y

Ajuste de frecuencia con zumbador
Controlas el sonido de un zumbador con cuatro botones: uno lo enciende, otro lo calla y los otros dos lo hacen más agudo o más grave. Si mantienes pulsado el botón de subir o bajar, el tono cambia pas

Sensor de aparcamiento con HC-SR04
Una versión en miniatura del sensor de aparcamiento de los coches: cuanto más se acerca algo, más rápidos son los pitidos. El sensor de distancia mide el objeto que tiene delante; si está a más de 20

ESP32 HC-SR04 Medidor de distancia por ultrasonidos
El sensor ultrasónico HC-SR04 calcula la distancia midiendo el tiempo de vuelo (Time of Flight) de la onda sonora que emite al chocar contra un objeto y rebotar. En este proyecto, enviaremos un pulso de 10 microsegundos desde el ESP32 al pin TRIG del sensor y leeremos el tiempo de eco procedente del pin ECHO mediante pulseIn(). Mostraremos la distancia calculada en centímetros en una pantalla LCD I2C de 16x2. Dado que el HC-SR04 funciona a 5 V, es obligatorio utilizar un divisor de tensión

Sensor de aparcamiento ESP32 (HC-SR04 + barra de 5 LED + zumbador + OLED)
¡La versión ESP32 del sensor de aparcamiento para coches! Medimos la distancia al objeto que hay delante mediante el sensor ultrasónico HC-SR04. A medida que la distancia disminuye, la barra de 5 LED se va llenando (verde → amarillo → rojo) y la frecuencia de los pitidos del zumbador se acorta. Cuando se acerca demasiado (menos de 10 cm), suena una alarma continua y se encienden todos los LED. En la pantalla OLED SSD1306 se muestra, en letra grande, el valor actual en centímetros, un mensaje de

Robot para evitar obstáculos
¿Qué le parece construir un vehículo inteligente que encuentre su propio camino? El Robot Evitador de Obstáculos explora constantemente su entorno gracias al sensor ultrasónico que lleva delante. Cuan

Sistema de Alarma de Movimiento ESP32 (PIR + Buzzer + LED + Telegram)
Un sistema inteligente de alarma de seguridad que activa el buzzer y el LED cuando el sensor PIR detecta movimiento, y envía simultáneamente una notificación instantánea al propietario vía Telegram. Cuenta con un tiempo de espera de 30 segundos para limitar la frecuencia de las alarmas repetidas; este mecanismo no comprueba la validez de las detecciones del sensor PIR ni elimina las falsas detecciones. Ideal para seguridad del hogar, vigilancia de garajes, almacenes o salas de trabajo. Con ESP32
Más artículos
App BlueGrays
Monta este circuito paso a paso en tu teléfono
Esquema, código y lista de piezas en una sola pantalla. Más de 200 proyectos de Arduino, ESP32 y Pico listos para montar.
Escanea con tu teléfono
Apunta la cámara al código y se abrirá la tienda de apps de tu teléfono.
o ábrela en la tienda

