Guías · 28 min de lectura

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.

Sensor ultrasónico de distancia HC-SR04 para un bastón inteligente
El HC-SR04 emite un pulso de sonido de 40 kHz y calcula la distancia a partir del tiempo que tarda en volver el eco. En el bastón inteligente vigila obstáculos entre 2 y 150 cm dentro de un cono estrecho de unos 15 grados.

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

ComponenteFunciónNota
Arduino Nano (Uno para el prototipo)Mide, decide, controla el motor y el zumbadorEl Nano encaja en un bastón por ser pequeño y ligero
Sensor ultrasónico HC-SR04Mide la distancia al obstáculo de delante5 V, unos 15 mA; JSN-SR04T para lluvia
Módulo de motor de vibraciónAvisa del obstáculo a la mano con vibraciónMódulo listo con transistor y diodo
Zumbador pasivoAvisos de agua, caída, SOS y bateríaPasivo, para poder tocar tonos distintos
Sensor de nivel de aguaAvisa cuando la punta entra en el agua1–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á tumbadoI2C, dirección 0x68
PulsadorSonido sí/no, cancelar alarma, SOS con pulsación largaColocado bajo el pulgar
Celda 18650 + TP4056 + MT3608Alimentación portátil, carga por USB, elevación a 5 VUsa un TP4056 con protección (DW01)
2 resistencias de 100 kΩDivisor para medir la tensión de la bateríaEl punto medio va a A0
Interruptor y cajaEncendido/apagado y protecciónImpresa en 3D o una cajita de proyectos
Arduino Nano
Arduino Nano
Zumbador pasivo
Zumbador pasivo
Pulsador
Pulsador
Batería Li-ion 18650
Batería Li-ion 18650

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 mObstáculos típicos que detecta
0°Unos 55–95 cmBancos, papeleras, la carrocería de un coche aparcado
15°Unos 95–135 cmEl torso de una persona, el manillar de una bici, la caja de un camión
25°Unos 120–170 cmCarteles, 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.

SensorAlcanceVentajasInconvenientes
HC-SR042–400 cmBarato, sencillo, cono amplioNo es impermeable, le cuestan las superficies blandas
JSN-SR04TUnos 25–450 cmSonda impermeable, funciona con lluviaNo ve a menos de 25 cm, más caro
VL53L1X (láser ToF)Hasta 4 mPequeño, ligero, haz definidoEl alcance baja con luz solar, I2C a 3,3 V
Sharp GP2Y0A02 (infrarrojo)20–150 cmSalida analógica, código sencilloSe confunde con superficies oscuras y brillantes, consume unos 33 mA
Conoce los puntos ciegos del sensor ultrasónico: las superficies blandas como un abrigo grueso, un arbusto o una cortina absorben el sonido, mientras que una puerta de cristal o una placa inclinada lo desvían, de modo que el obstáculo puede no aparecer. Los postes finos de 3–4 cm también se detectan tarde porque son pequeños dentro del cono. Por eso nunca presentes un bastón inteligente como única ayuda de seguridad: es un asistente que trabaja junto al bastón blanco.

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.

DistanciaIntervalo entre pulsosFuerza de vibración (PWM)Significado para el usuario
Más de 150 cmSin vibración0Camino libre
150 cm900 ms150Hay un obstáculo más adelante
100 cm575 ms194El obstáculo se acerca, atención
60 cm315 ms229Frena y prepárate para cambiar de dirección
40 cm185 ms247Muy cerca
30 cm o menosContinua255Para
Módulo de motor de vibración para Arduino
Módulo de motor de vibración: junto al motor tipo moneda, de unos 10 mm de diámetro, están el transistor que lo controla y un diodo de protección. El pin IN se conecta directamente a un pin PWM del Arduino y el módulo soporta la corriente del motor.

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ónArduino Uno / NanoESP32Raspberry Pi Pico
HC-SR04 VCC5VVIN (5 V)VBUS o línea de 5 V
HC-SR04 TRIGD9GPIO 18GP2
HC-SR04 ECHOD10 (directo)GPIO 19, con divisor 1 kΩ / 2 kΩGP3, con divisor 1 kΩ / 2 kΩ
Módulo de vibración IND5 (PWM)GPIO 25GP15
Zumbador pasivo (+)D8GPIO 26GP16
Pulsador (otra pata a GND)D2GPIO 27GP14
Sensor de agua +D7GPIO 33GP22
Sensor de agua SA1GPIO 34 (ADC1)GP26 (ADC0)
MPU6050 SDAA4GPIO 21GP4
MPU6050 SCLA5GPIO 22GP5
MPU6050 VCC5V3V33V3
Medida de batería (punto medio del divisor 100k/100k)A0GPIO 35 (ADC1)GP28 (ADC2)
GPS NEO-6M TX / RXNoGPIO 16 / GPIO 17No
SIM800L TXD / RXDNoGPIO 13 / GPIO 23No
El pin ECHO del HC-SR04 da un pulso de 5 V, pero los pines del ESP32 y de la Pico trabajan a 3,3 V y no toleran 5 V. Monta un divisor de tensión así: ECHO → 1 kΩ → cable a la placa → 2 kΩ → GND, y al pin llegarán unos 3,3 V. En el ESP32 usa los pines del ADC1 (GPIO 32–39) para lecturas analógicas, porque el ADC2 no funciona con el Wi-Fi activado. En las placas ESP32 con módulo WROVER, los GPIO 16 y 17 están conectados a la PSRAM; si tienes una, lleva el GPS a otros dos pines. Todos los módulos deben compartir GND.

Montaje paso a paso: primero el bastón básico

  1. 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.
  2. Conecta el VCC y el GND del HC-SR04 a los raíles, TRIG a D9 y ECHO a D10.
  3. 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.
  4. 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.
  5. 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.
  6. Cuando todo funcione, añade el sensor de agua, el MPU6050 y el pulsador y pasa al código 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}

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.

Sensor de nivel de agua para Arduino
Sensor de nivel de agua: cuando el agua une las pistas de cobre entrelazadas, sube la tensión en el pin S. En el bastón inteligente va 1–2 cm por encima de la punta y solo se alimenta en el momento de medir.
Cubre la parte superior sin pistas del sensor y su conector con silicona caliente o esmalte de uñas; el agua solo debe tocar las pistas que miden. No dejes el cable colgando por fuera del bastón: pásalo por dentro de un bastón de aluminio o enróllalo a la caña dentro de un tubo termorretráctil. Si quieres una solución más resistente, pon dos tornillos de acero inoxidable separados 1 cm justo encima de la punta: uno va a D7 y el otro a A1, que se conecta a GND con una resistencia de 100 kΩ. Cuando el agua une los tornillos, A1 sube; el código no cambia, solo tienes que volver a medir WATER_LIMIT.

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.

Módulo acelerómetro y giroscopio MPU6050 (GY-521)
MPU6050 (GY-521): acelerómetro y giroscopio de tres ejes. Se fija dentro de la caja con la flecha X impresa en la placa a lo largo de la caña del bastón.

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.

FaseCondiciónDuración¿Qué pasa?
WALK (marcha)La aceleración total supera 2,5 gSin límitePasa a SETTLE; los avisos de obstáculos siguen
SETTLE (reposo)Espera a que acabe la sacudida1 sPasa a CHECK
CHECK (comprobación)La inclinación debe seguir por encima de 70° y la aceleración entre 0,8 y 1,2 g2 sSi falla la condición vuelve a WALK; si se cumple, ALARM
ALARMUn pitido y una vibración cada segundo para encontrar el bastón20 sSe 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ónHasta que se canceleUna 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.

Este método detecta que se ha caído el bastón, no la persona. Si alguien cae sin soltar el bastón o este queda apoyado en una pared, la alarma no salta; por eso conserva siempre el botón de SOS manual y explica claramente esta limitación en tu presentación. Para medir directamente la caída del cuerpo, el sensor tendría que ir en un dispositivo aparte en el cinturón o la muñeca.

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.

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. 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.
  2. 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).
  3. 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.
  4. 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.
  5. 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.

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}
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_MSSensibilidad 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ónPesoAutonomía aprox. (50 mA)Valoración
Pila de 9 V (unos 500 mAh), a VIN45 g8–10 horasNo recomendada: la tensión cae cuando arranca el motor y no es recargable
4 pilas AA NiMH (2000 mAh), al pin 5VUnos 120 g35–40 horasPesada; recién cargada llega a 5,6 V y fuerza el límite
1 celda 18650 + TP4056 + MT3608Unos 55 g25–30 horasRecomendada: ligera, carga por USB, protegida
2 celdas 18650 en serie (7,4 V), a VINUnos 105 g45–50 horasMucha autonomía pero pesada, necesita su propio circuito de carga
Powerbank (10 000 mAh)200 g o másVarios díasPesada; la mayoría de powerbanks se apagan con poca corriente
Módulo de carga TP4056 para baterías de litio
Módulo de carga TP4056: carga la celda 18650 por USB hasta 4,2 V con 1 A. La versión con el chip de protección DW01 también protege la batería contra la descarga excesiva y los cortocircuitos; la carga se conecta a OUT+ y OUT−.

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.

La salida del MT3608 puede ajustarse con su trimmer hasta 28 V, y el módulo puede venir de fábrica con cualquier valor. Antes de conectarlo al Nano, mide la salida con un multímetro y gira el trimmer hasta 5,0 V; un elevador mal ajustado destruye la placa en segundos. Si durante las pruebas alimentas la placa solo por USB, A0 queda al aire, la lectura de batería es aleatoria y puede que oigas el pitido de batería baja; es normal.

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.

ComponentePeso aprox.
Arduino Nano7 g
HC-SR049 g
MPU6050 (GY-521)2 g
Módulo de vibración + zumbador pasivo5 g
Sensor de nivel de agua y cable8 g
Celda 1865045–48 g
TP4056 + MT3608 + interruptor8 g
Caja impresa en 3D y abrazaderas40–50 g
Cables, tornillos, cinta de velcro10–15 g
TotalUnos 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.

Módulo GPS NEO-6M con su antena cerámica
Módulo GPS NEO-6M: la antena cerámica necesita ver el cielo, por eso se coloca en la parte superior de la caja, lejos de piezas metálicas. La velocidad por defecto es de 9600 baudios.

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

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}

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.

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)

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.

  1. 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.
  2. 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.
  3. 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.
  4. Resultados: Presenta los resultados de las pruebas de abajo en tablas y gráficos, e incluye los casos en que falló.
  5. 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.
PruebaCómo se haceValor que anotar
Precisión de distancia10 lecturas a 30, 60, 100 y 150 cm de una paredError medio (cm) y desviación máxima
Tipos de obstáculoPared, persona, poste de 5 cm, arbusto, puerta de cristal, cortina de telaCuántas veces de 10 se detecta cada obstáculo
Tiempo de respuestaVídeo a cámara lenta con el móvil: aparición del obstáculo y primera vibraciónRetraso en milisegundos
Detección de aguaSumergir la punta 0,5, 1, 2 y 3 cm en aguaA partir de qué profundidad avisa
Detección de caídas20 caídas y 100 golpes contra el sueloNúmero de alarmas correctas y falsas
AutonomíaFuncionar desde carga completa hasta que se apagueTiempo en horas y tensión final
Peso y equilibrioPesar con una báscula de cocina, medir la distancia al mangoPeso añadido (g) y par (kg·cm)
Haz las pruebas con usuarios con voluntarios con los ojos vendados, en un pasillo vacío y seguro y siempre con un acompañante al lado; nunca pruebes cerca del tráfico. Prepara un formulario de consentimiento para cada prueba con personas y comprueba qué documentos éticos piden las bases del concurso. Lo más probable es que el jurado pregunte "¿detecta las puertas de cristal?", "¿qué pasa con lluvia?" y "¿qué pasa cuando se acaba la batería?"; prepara las respuestas con los datos de tus tablas de pruebas.

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íntomaCausa probableSolución
El bastón vibra sin pararEl sensor apunta al suelo o al propio bastónSube el ángulo de montaje y deja libre el espacio delante del sensor
Vibraciones sueltas sin motivoAlgún eco erróneo aislado u otra fuente ultrasónica cercanaEl filtro de mediana ya está; si hace falta, usa la mediana de 5 lecturas
El motor no gira a distancias largasEl valor PWM está por debajo del umbral de arranque del motorMantén la fuerza mínima por encima de 150 y fija el motor con goma
El Arduino se reinicia cuando arranca el motorLa corriente del motor hunde la alimentación o falta el diodoUsa un módulo listo y añade un condensador de 100 µF a la alimentación
El MPU6050 siempre lee 0 o un valor fijoSDA y SCL cambiados, dirección 0x69 o chip dormidoBusca la dirección con un escáner I2C y comprueba que se escribe 0 en el registro 0x6B
Falsas alarmas al golpear el sueloIMPACT_G bajo y umbral de inclinación cercano al ángulo de marchaMide 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 suenaUmbral incorrecto o pistas oxidadasVuelve a medir los valores en seco y mojado y cambia el sensor si hace falta
El zumbador no suena o siempre da el mismo tonoEstá montado un zumbador activoUsa un zumbador pasivo para tone()
El SMS no se envíaEl SIM800L no está registrado en la red, la alimentación es débil o el PIN de la SIM está activadoComprueba que el LED del módulo parpadea cada 3 segundos, desactiva el PIN y alimenta el módulo desde la 18650
Al buscar fallos, prueba las piezas una a una: primero comprueba con el código de prueba que la distancia, el valor de agua y la inclinación tienen sentido, y después sube el código completo. Si el MPU6050 no responde, el escáner del artículo ¿Qué es I2C? encuentra su dirección. Si la placa se reinicia cuando empieza la vibración, la causa casi siempre es la alimentación; el artículo Arduino se reinicia solo resuelve este problema paso a paso.

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.

Ver todos los proyectos
HC-SR04 Medición de distancia (pantalla en serie)
#27Principiante

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

4.6(46)
71
Uso del zumbador
#13Principiante

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

4.7(89)
161
Ajuste de frecuencia con zumbador
#14Intermedio

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

4.6(69)
118
Sensor de aparcamiento con HC-SR04
#28Intermedio
Premium

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

4.8(64)
85
ESP32 HC-SR04 Medidor de distancia por ultrasonidos
#116Intermedio

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

4.9(28)
45
Sensor de aparcamiento ESP32 (HC-SR04 + barra de 5 LED + zumbador + OLED)
#142Avanzado
Premium

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

0.0(0)
14
Robot para evitar obstáculos
#81Avanzado
Premium

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

5.0(38)
49
Sistema de Alarma de Movimiento ESP32 (PIR + Buzzer + LED + Telegram)
#145Avanzado
Premium

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

0.0(0)
9