Умная трость на Arduino: оповещение о препятствиях, воде и падении
Как сделать умную трость для незрячих на Arduino: обнаружение препятствий на HC-SR04, вибросигнал, датчик воды, обнаружение падения на MPU6050, SOS и полный код.
Умная трость на Arduino предупреждает незрячего человека о препятствиях, которые не обнаружить белой тростью, и это один из самых популярных электронных проектов для школьных заданий и научных выставок. Ультразвуковой датчик HC-SR04, закреплённый у рукоятки, измеряет расстояние до того, что находится впереди; по мере приближения препятствия вибромотор в рукоятке срабатывает всё чаще и сильнее, а по желанию зуммер добавляет к каждому импульсу короткий писк. Так пользователь за несколько шагов чувствует препятствия выше пояса, которых кончик трости никогда не касается.
В этом руководстве начнём с базовой схемы, а затем шаг за шагом добавим то, что делает проект по-настоящему полезным: датчик луж у кончика трости, алгоритм на MPU6050, который замечает, что трость упала, и подаёт SOS, если за 20 секунд никто не отменит тревогу, схему питания, которая работает больше 25 часов от одной ячейки 18650, корпус, продуманный с учётом веса и эргономики, и расширение на ESP32 + GPS, которое отправляет координаты по SMS. В конце есть конкретные советы, как испытать проект и представить его на научной выставке.

Что такое умная трость и что она делает?
Белая трость — инструмент, которому незрячий человек доверяет больше всего: каждое касание рассказывает о покрытии, бордюре, ступеньке или низком препятствии впереди. Но трость «видит» только то, чего касается её кончик. Препятствия выше пояса — низкая ветка дерева, вывеска над тротуаром, зеркало грузовика, открытое окно или кондиционер, торчащий из стены, — никогда не касаются кончика и вместо этого бьют по телу или голове. Умная трость создана, чтобы закрыть этот пробел: электронный датчик просматривает высоты и расстояния, недоступные трости, и передаёт результат в руку вибрацией.
Сразу проясним одну вещь: умная трость не заменяет белую трость и обучение ориентированию и мобильности, а дополняет их. У ультразвукового датчика есть слепые зоны, а батареи садятся, поэтому золотое правило конструкции такое: трость должна работать точно так же, как раньше, даже когда электроника полностью выключена. Международный день белой трости, который отмечают каждый год 15 октября, — отличный повод показать этот проект в школе.
Как работает умная трость?
Проект состоит из одного цикла, который выполняется сотни раз в секунду и одновременно ведёт четыре независимые задачи. Каждые 60 миллисекунд Arduino измеряет расстояние, берёт медиану трёх последних показаний и по ней подстраивает частоту вибрации. Каждые полсекунды она опрашивает датчик воды на кончике трости. Пятьдесят раз в секунду считывает ускорения MPU6050, чтобы проверить, не упала ли трость. А каждые тридцать секунд проверяет напряжение батареи. У конструкции два канала вывода с разными задачами: вибрация — только для препятствий, а звук сообщает об особых событиях: вода, падение, SOS и разряд батареи. Короткое нажатие кнопки по желанию включает или выключает писк при препятствиях.
| Деталь | Задача | Примечание |
|---|---|---|
| Arduino Nano (Uno для макета) | Измеряет, принимает решения, управляет мотором и зуммером | Nano подходит для трости: она маленькая и лёгкая |
| Ультразвуковой датчик HC-SR04 | Измеряет расстояние до препятствия впереди | 5 V, около 15 mA; для дождя — JSN-SR04T |
| Модуль вибромотора | Сообщает руке о препятствии вибрацией | Готовый модуль с транзистором и диодом |
| Пассивный зуммер | Сигналы о воде, падении, SOS и батарее | Пассивный, поэтому играет разные тона |
| Датчик уровня воды | Предупреждает, когда кончик трости попадает в воду | На 1–2 см выше кончика, питание только во время измерения |
| MPU6050 (GY-521) | Определяет, что трость упала и лежит | I2C, адрес 0x68 |
| Кнопка | Звук вкл/выкл, отмена тревоги, SOS долгим нажатием | Под большим пальцем |
| Ячейка 18650 + TP4056 + MT3608 | Автономное питание, зарядка по USB, повышение до 5 V | Бери TP4056 с защитой (с DW01) |
| 2 резистора по 100 kΩ | Делитель для измерения напряжения батареи | Средняя точка идёт на A0 |
| Выключатель и корпус | Включение/выключение и защита | Напечатанный на 3D-принтере или небольшая коробка для проектов |




Обнаружение препятствий: как измеряет HC-SR04 и куда его ставить
Получив импульс длительностью 10 микросекунд на пин TRIG, HC-SR04 посылает восемь звуковых волн частотой 40 kHz и держит пин ECHO в HIGH, пока не вернётся эхо. При 20 °C звук проходит около 343 метров в секунду; с учётом пути туда и обратно на каждый сантиметр приходится примерно 58 микросекунд. Поэтому расстояние считается просто: см = время (µs) / 58. Датчик видит внутри конуса примерно в 15 градусов; в 1,5 метра впереди ширина этого конуса около 40 см. Поскольку датчик поворачивается вместе с тростью, когда ею водят из стороны в сторону, он сам собой просматривает впереди полосу шириной около метра. Подробности о датчике — в статье Датчик расстояния HC-SR04.
Самая важная деталь установки — угол. При ходьбе трость держат не вертикально, а с наклоном вперёд примерно на 40–50 градусов от вертикали. Если приклеить датчик параллельно трости, он смотрит в землю и постоянно принимает покрытие за препятствие. Закрепи коробку ближе к рукоятке, примерно в 75 см над землёй, и направь датчик чуть выше горизонтали. При наклоне трости в 45 градусов ось датчика должна составлять с древком угол 60 градусов (отсчитывая в сторону кончика), чтобы датчик смотрел на 15 градусов выше горизонтали. Проще всего найти правильный угол так: держи трость обычным хватом и приложи телефон с приложением-уровнем к передней стенке коробки.
| Угол датчика (над горизонталью) | Высота просмотра в 1,5 м впереди | Типичные препятствия, которые он ловит |
|---|---|---|
| 0° | Около 55–95 см | Скамейки, урны, кузов припаркованной машины |
| 15° | Около 95–135 см | Туловище человека, руль велосипеда, кузов грузовика |
| 25° | Около 120–170 см | Вывески, ветки деревьев, открытые створки окон, боковые зеркала |
Если начинать с одного датчика, хороший компромисс — 15–20 градусов: так уверенно ловятся препятствия на уровне тела. А вот препятствия на уровне головы попадают в верхний край конуса, только пока они ещё далеко, и выпадают из него по мере приближения. Если важна высота головы, выдели для неё второй HC-SR04, наклонённый вверх на 30–35 градусов; поскольку этот датчик меряет наискосок, ограничь его дальность примерно 200 см. Не забудь запускать два датчика по очереди с интервалом не меньше 60 мс, иначе один услышит эхо другого. Подробное сравнение с лазерными (ToF) и инфракрасными датчиками — в статье сравнение датчиков расстояния.
| Датчик | Дальность | Плюсы | Минусы |
|---|---|---|---|
| HC-SR04 | 2–400 см | Дешёвый, простой, широкий конус | Не водонепроницаемый, плохо видит мягкие поверхности |
| JSN-SR04T | Около 25–450 см | Водонепроницаемый зонд, работает под дождём | Не видит ближе 25 см, дороже |
| VL53L1X (лазерный ToF) | До 4 м | Маленький, лёгкий, узкий луч | На солнце дальность падает, I2C на 3,3 V |
| Sharp GP2Y0A02 (инфракрасный) | 20–150 см | Аналоговый выход, простой код | Ошибается на тёмных и блестящих поверхностях, потребляет около 33 mA |
Частота вибрации и писка в зависимости от расстояния
Незрячий человек во многом воспринимает окружение на слух: приближающаяся машина, гул толпы, эхо постукивания трости от стены помогают ему находить дорогу. Зуммер, который пищит без остановки, скорее заглушает эти звуки, чем помогает. Поэтому главный канал оповещения в этом проекте — вибрация, и правило простое: чем ближе препятствие, тем чаще и сильнее виброимпульсы. Всё, что дальше 150 см, игнорируется, а если препятствие ближе 30 см, мотор вибрирует непрерывно. В промежутке интервал между импульсами вычисляется линейно через map(): интервал = 120 + (расстояние − 30) × 6,5 мс.
Почему 150 см? Человек с белой тростью проходит около 1 метра в секунду. Дальность 150 см даёт ему примерно 1,5 секунды до препятствия; поскольку время реакции на тактильный сигнал — 0,2–0,3 секунды, этого хватает, чтобы остановиться или сменить направление. Если растянуть дальность до 250 см, на людном тротуаре трость вибрирует без перерыва, и через какое-то время пользователь начинает игнорировать сигналы. В помещении хорошо работает 100 см, на улице — 150–200 см; менять нужно только константу FAR_CM.
| Расстояние | Интервал импульсов | Сила вибрации (ШИМ) | Что это значит для пользователя |
|---|---|---|---|
| Дальше 150 см | Нет вибрации | 0 | Путь свободен |
| 150 см | 900 мс | 150 | Впереди препятствие |
| 100 см | 575 мс | 194 | Препятствие приближается, осторожно |
| 60 см | 315 мс | 229 | Сбавь шаг, приготовься сменить направление |
| 40 см | 185 мс | 247 | Совсем близко |
| 30 см и ближе | Непрерывно | 255 | Стоп |

Чаще всего в тростях ставят плоский ERM-мотор-«таблетку», как в мобильных телефонах. Он работает примерно от 3 V и потребляет около 60–80 mA. А пин Arduino может отдать максимум 40 mA, а безопасный предел — 20 mA, поэтому мотор подключают не к пину напрямую, а через транзистор. В готовых модулях уже есть этот транзистор и диод, который гасит обратное напряжение при остановке мотора; большинство модулей работает от 3,3–5 V. Если мотор голый, хватит NPN-транзистора вроде BC337 или 2N2222, базового резистора 1 kΩ и диода 1N4148, включённого встречно параллельно мотору; транзисторы можно сравнить в утилите распиновки транзисторов.
Место мотора важно не меньше кода. Размести его внутри рукоятки, там, где лежит ладонь или указательный палец, и закрепи тонкой резиной, а не поролоном: поролон гасит вибрацию. На полной скорости мотор-«таблетка» вибрирует с частотой около 200 Hz, а кожа сильнее всего чувствует вибрацию около 200–300 Hz, так что даже маленький мотор в правильном месте ощущается легко. ERM-моторы не могут стронуться при очень малых значениях ШИМ, поэтому самая слабая вибрация в коде — 150 из 255. О выборе мотора и управлении силой вибрации через ШИМ — в статье вибромотор.
Схема подключения: Arduino Uno/Nano, ESP32 и Raspberry Pi Pico
Таблица ниже точно соответствует всему коду в статье. Номера пинов на Uno и Nano одинаковые; на обеих платах шина I2C для MPU6050 — на A4 (SDA) и A5 (SCL). VCC вибромодуля идёт на 5V, а его пин IN намеренно посажен на D5: tone() использует Timer2, поэтому ШИМ на D3 и D11 перестаёт работать, пока играет зуммер, а D5 относится к Timer0, и его это не касается. Карту пинов Nano можно посмотреть в утилите распиновки.
| Подключение | Arduino Uno / Nano | ESP32 | Raspberry Pi Pico |
|---|---|---|---|
| HC-SR04 VCC | 5V | VIN (5 V) | VBUS или шина 5 V |
| HC-SR04 TRIG | D9 | GPIO 18 | GP2 |
| HC-SR04 ECHO | D10 (напрямую) | GPIO 19 через делитель 1 kΩ / 2 kΩ | GP3 через делитель 1 kΩ / 2 kΩ |
| Вибромодуль IN | D5 (ШИМ) | GPIO 25 | GP15 |
| Пассивный зуммер (+) | D8 | GPIO 26 | GP16 |
| Кнопка (второй вывод на GND) | D2 | GPIO 27 | GP14 |
| Датчик воды + | D7 | GPIO 33 | GP22 |
| Датчик воды S | A1 | GPIO 34 (ADC1) | GP26 (ADC0) |
| MPU6050 SDA | A4 | GPIO 21 | GP4 |
| MPU6050 SCL | A5 | GPIO 22 | GP5 |
| MPU6050 VCC | 5V | 3V3 | 3V3 |
| Контроль батареи (средняя точка делителя 100k/100k) | A0 | GPIO 35 (ADC1) | GP28 (ADC2) |
| GPS NEO-6M TX / RX | Нет | GPIO 16 / GPIO 17 | Нет |
| SIM800L TXD / RXD | Нет | GPIO 13 / GPIO 23 | Нет |
Пошаговая сборка: начнём с базовой трости
- Сначала собери схему на макетной плате, а установку на трость оставь на самый конец. Вставь Arduino Nano в середину макетной платы и выведи 5V и GND на боковые шины.
- Подключи VCC и GND датчика HC-SR04 к шинам, TRIG — к D9, ECHO — к D10.
- Подключи VCC вибромодуля к 5V, GND — к GND, а пин IN (на некоторых модулях S) — к D5. Плюсовую ножку пассивного зуммера вставь в D8, а вторую — в GND.
- Загрузи базовый код ниже и открой монитор порта на скорости 9600 бод. Медленно приближай руку к датчику и отводи её; расстояние на экране должно меняться, а вибрация — ускоряться и замедляться вместе с ним.
- Держи датчик в 150, 100, 60 и 30 см от стены и сравни значение в мониторе порта с рулеткой. Если разница больше 1–2 см, проверь подключение и питание.
- Когда всё заработает, добавь датчик воды, MPU6050 и кнопку и переходи к полному коду.
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}В коде нигде нет длинного delay(): каждая задача работает по отметкам времени, которые ведутся через millis(), поэтому виброимпульсы никогда не тормозят измерения. Для pulseIn() выбран таймаут 12 мс: так как 12 000 / 58 ≈ 200 см, эхо с большего расстояния нам всё равно не нужно, а короткий таймаут не даёт циклу зависнуть. Если эхо не вернулось, функция возвращает 999, и это считается «путь свободен». setVib() пишет в мотор только при изменении значения; поскольку analogWrite() в версиях 2.x ядра ESP32 при каждом вызове заново настраивает блок ШИМ, эта мелочь позволяет тому же коду плавно работать и там.
Датчик луж: сигнал, когда кончик трости попадает в воду
Лужи — одни из самых трудных для белой трости препятствий: когда кончик попадает в воду, звук и ощущение почти не отличаются от земли, а нескольких сантиметров воды хватает, чтобы промочить обувь. Для этого используем дешёвый датчик уровня воды с чередующимися медными дорожками. Одни дорожки подключены к питанию, другие — к сигнальной линии; когда вода замыкает их, напряжение сигнала растёт и значение analogRead() увеличивается. Сухой датчик показывает около 0, а датчик, опущенный концом в воду, на Uno обычно даёт несколько сотен; чем глубже, тем больше значение.
Закрепи датчик в самом низу трости так, чтобы его нижний край был примерно на 1–2 см выше кончика, а дорожки — перпендикулярны земле. Тогда на сухой земле его ничего не касается, а в луже глубже 1–2 см дорожки намокают. Известная слабость таких датчиков — медные дорожки быстро разъедает электролиз, если на них постоянно подано питание. Решение в коде — подключить пин + датчика не прямо к 5V, а к D7 и подавать питание только на 10 мс измерения. При одном измерении каждые полсекунды датчик под напряжением всего 2% времени. Как он работает, рассказано в статье датчик уровня воды, а чем отличается от платы дождя — в статье датчик дождя.

Обнаружение падения на MPU6050
MPU6050 — датчик движения с трёхосевым акселерометром и трёхосевым гироскопом на одном кристалле, подключается по I2C. На плате GY-521 есть стабилизатор, поэтому она работает от 5 V; адрес по умолчанию — 0x68. В этом проекте используем только акселерометр и вместо библиотеки обращаемся напрямую к четырём регистрам: запись 0 в 0x6B будит микросхему, 0x1A включает встроенный фильтр 44 Hz, запись 0x10 в 0x1C расширяет диапазон измерения до ±8 g, а ускорения X, Y и Z читаем из шести байтов, начиная с 0x3B. Стандартный диапазон ±2 g легко зашкаливает в момент удара трости о землю и скрывает реальную силу удара. В диапазоне ±8 g каждому 1 g соответствует 4096 единиц сырого значения. Чтобы лучше познакомиться с датчиком, загляни в статью акселерометр и гироскоп MPU6050.

При каждом измерении код вычисляет две величины. Первая — полное ускорение: g = √(ax² + ay² + az²). Пока трость неподвижно держат в руке или она лежит на земле, оно около 1 g; при сильном ударе подскакивает до нескольких g. Вторая — наклон, угол между тростью и вертикалью. Если установить плату стрелкой X вдоль древка, наклон равен atan2(√(ay² + az²), |ax|): 0 градусов у вертикальной трости, 40–55 при ходьбе и 85–90, когда она лежит на земле. Системы, обнаруживающие падение человека, сначала ищут короткое свободное падение (момент, близкий к 0 g); но падающая трость движется как палка, вращающаяся вокруг кончика, поэтому датчик у рукоятки часто этого момента не видит. Поэтому наш алгоритм ищет не свободное падение, а удар, за которым следует ровное неподвижное положение.
| Этап | Условие | Длительность | Что происходит? |
|---|---|---|---|
| WALK (ходьба) | Полное ускорение превышает 2,5 g | Без ограничений | Переход в SETTLE; сигналы о препятствиях продолжают работать |
| SETTLE (успокоение) | Ожидание, пока утихнет тряска | 1 с | Переход в CHECK |
| CHECK (проверка) | Наклон должен оставаться больше 70°, а ускорение — от 0,8 до 1,2 g | 2 с | Если условие нарушено — назад в WALK, если выполняется — ALARM |
| ALARM (тревога) | Писк и вибрация каждую секунду, чтобы трость можно было найти | 20 с | Отменяется нажатием кнопки или подъёмом трости (наклон меньше 60°) |
| SOS | ... --- ... азбукой Морзе звуком, светодиодом и вибрацией | До отмены | Короткое нажатие останавливает; ESP32 отправляет SMS |
Такая последовательность отсекает большинство ложных срабатываний. Постукивание тростью о землю при ходьбе тоже может давать короткие удары больше 2,5 g, но через секунду трость всё ещё в руке и наклонена на 40–55 градусов, поэтому этап проверки сразу отменяется; пользователь даже не заметит. А вот когда трость падает и остаётся лежать, тревога начинается примерно через 3 секунды, и писк каждую секунду помогает пользователю, у которого выпала трость, найти её на слух. Если трость поднять, тревога прекращается сама. Если 20 секунд никто не реагирует, код считает, что это может быть настоящее падение, и запускает SOS. Удержание кнопки 2 секунды тоже в любой момент запускает SOS вручную.
Код для тестирования и калибровки
Пороги немного отличаются для каждой трости, каждой поверхности и каждого датчика. Прежде чем переходить к основному коду, загрузи скетч ниже: каждую четверть секунды он выводит в монитор порта расстояние, сырое значение датчика воды, максимальное ускорение за последнюю четверть секунды и наклон. Если записывать значения в таблицу, заодно заполнится раздел результатов в отчёте по проекту.
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 см в стакан воды. Если сухой он показывает, например, 0–20, а мокрый 520, выбери для WATER_LIMIT середину — 270–300.
- Держи трость обычным хватом и води ею из стороны в сторону, как при ходьбе. Запиши наклон: обычно он держится между 40 и 55 градусами и должен быть меньше PICKUP_DEG (60).
- Положи трость на пол. Наклон должен показывать 85–90, а ускорение — около 0,95–1,05 g; это подтверждает LYING_DEG (70) и окно 0,8–1,2 g.
- Постучи тростью о землю, как обычно при ходьбе, и следи за пиковыми значениями g. Затем 10 раз урони трость на коврик. Установи IMPACT_G ниже значений при падении; даже если постукивания превысят этот порог, их отфильтрует проверка наклона.
- Сделай по 10 измерений на расстоянии 30, 60, 100 и 150 см от стены и сравни среднее с рулеткой.
Полный код: препятствия, вода, падение, SOS и предупреждение о разряде батареи
В полном коде четыре задачи работают в одном цикле, не мешая друг другу, а обнаружение падения — это небольшой конечный автомат на enum и switch-case. У кнопки три задачи: при ходьбе короткое нажатие включает или выключает писк при препятствиях (вибрация остаётся включённой всегда), во время тревоги или SOS короткое нажатие отменяет их, а долгое нажатие на 2 секунды в любой момент запускает SOS. Высокий писк означает «звук включён», низкий — «звук выключен».
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}| Функция или параметр | Что делает? |
|---|---|
| pingCm() (замер расстояния) | Делает одно измерение; возвращает 999, если эхо не пришло за 12 мс |
| median3() (медиана трёх) | Берёт медиану трёх последних измерений, отбрасывая случайное плохое значение |
| readMpu() (чтение MPU6050) | Читает шесть байтов данных ускорения и вычисляет полное ускорение и наклон |
| fallUpdate() (контроль падения) | Ведёт конечный автомат WALK → SETTLE → CHECK → ALARM → SOS |
| waterUpdate() (контроль воды) | Подаёт питание на датчик воды на 10 мс, считывает его и включает сигнал при переходе из сухого состояния в мокрое |
| walkOutput() (сигналы при ходьбе) | Выдаёт вибрацию в зависимости от расстояния и сигнал о воде двухтональным звуком |
| sosOutput() (вывод SOS) | Шаг за шагом проигрывает массив SOS_PATTERN |
| readButton() (опрос кнопки) | Различает короткое (1) и долгое (2) нажатие и игнорирует дребезг короче 30 мс |
| IMPACT_G, LYING_DEG, CANCEL_MS (пороги) | Чувствительность к падению и время отмены; подбери их тестовым скетчем |
Сигнал SOS следует правилам азбуки Морзе: точка — 1 единица, тире — 3 единицы, между сигналами 1 единица, между буквами 3, между повторами 7. При единице 150 мс один цикл SOS занимает 34 единицы, то есть около 5,1 секунды. Тон 3 kHz выбран намеренно: человеческое ухо наиболее чувствительно в диапазоне от 2 до 4 kHz, и маленькие пассивные зуммеры громче всего звучат тоже в этом диапазоне. Чем активный зуммер отличается от пассивного, рассказано в статье Что такое зуммер?.
Батарея, питание и время работы
Умной тростью пользуются на улице весь день, поэтому схема питания важна не меньше датчиков. Оценим средний ток: Nano потребляет около 20 mA, HC-SR04 — 15 mA, MPU6050 — 4 mA. Вибромотор тянет около 70 mA, но только во время импульсов; при обычной прогулке в среднем это около 10 mA. Зуммер и датчик воды добавляют несколько mA. Всего выходит примерно 50 mA, то есть 0,25 W при 5 V. Ячейка 18650 на 2600 mAh запасает около 9,6 Wh при 3,7 V; через повышающий преобразователь с КПД 85% до схемы доходит около 8 Wh. 8 / 0,25 = 32 часа; с учётом того, что батарею нельзя использовать полностью, реальная цифра — 25–30 часов. При использовании по два часа в день трость нужно заряжать примерно раз в две недели. Свой расчёт можно сделать в калькуляторе времени работы от батареи, а о разнице между mAh и Wh — прочитать в статье mAh и Wh.
| Источник питания | Вес | Примерное время работы (50 mA) | Вывод |
|---|---|---|---|
| Батарейка-«крона» (PP3) на 9 V (около 500 mAh), на VIN | 45 г | 8–10 часов | Не рекомендуется: напряжение проседает при работе мотора, не перезаряжается |
| 4 × AA NiMH (2000 mAh), на пин 5V | Около 120 г | 35–40 часов | Тяжело; при полной зарядке даёт 5,6 V, на грани допустимого |
| 1 × 18650 + TP4056 + MT3608 | Около 55 г | 25–30 часов | Рекомендуется: легко, зарядка по USB, есть защита |
| 2 × 18650 последовательно (7,4 V), на VIN | Около 105 г | 45–50 часов | Долго работает, но тяжело, нужна отдельная схема зарядки |
| Пауэрбанк (10 000 mAh) | 200 г и больше | Несколько дней | Тяжело; большинство пауэрбанков отключаются при малом токе |

Рекомендуемая цепь питания такая: ячейка 18650 подключается к клеммам B+ и B− модуля TP4056. Линия с выхода OUT+ модуля идёт через движковый выключатель на вход повышающего модуля MT3608. Выход MT3608, настроенный на 5,0 V, питает пин 5V у Nano, HC-SR04 и вибромодуль. Для измерения напряжения батареи сразу после выключателя стоит делитель из двух резисторов по 100 kΩ, средняя точка которого идёт на A0; поскольку он стоит после выключателя, он не разряжает батарею, когда трость выключена. Микросхема DW01 отключает ячейку примерно при 2,4–2,5 V, но код предупреждает гораздо раньше: ниже 3,4 V каждые 30 секунд звучит низкий писк. Подробнее — в статье модуль зарядки TP4056.
Корпус, вес и эргономика
Большинство складных белых тростей весят 200–300 г, и пользователь часами водит ими из стороны в сторону движением запястья. Каждый добавленный грамм умножается на расстояние от руки и нагружает запястье. Коробка 150 г в 10 см ниже рукоятки даёт на запястье дополнительный момент 150 × 10 = 1,5 кг·см; та же коробка на 60 см ниже дала бы 9 кг·см, в шесть раз больше. Поэтому собери всю электронику в одной коробке и размести её сразу под рукояткой; вниз до кончика идут только датчик воды и его тонкий кабель. Старайся, чтобы весь добавленный вес был меньше 150 г.
| Деталь | Примерный вес |
|---|---|
| Arduino Nano | 7 г |
| HC-SR04 | 9 г |
| MPU6050 (GY-521) | 2 г |
| Вибромодуль + пассивный зуммер | 5 г |
| Датчик уровня воды и кабель | 8 г |
| Ячейка 18650 | 45–48 г |
| TP4056 + MT3608 + выключатель | 8 г |
| Коробка и хомуты, напечатанные на 3D-принтере | 40–50 г |
| Провода, винты, ремешок на липучке | 10–15 г |
| Итого | Около 135–150 г |
- Материал: печатай коробку из PETG или ABS; PLA может размягчиться и покоробиться в машине, оставленной на солнце. Толщины стенки 1,6–2 мм достаточно.
- Крепление: закрепи коробку на трости двумя червячными хомутами или ремешком на липучке с резиновой подкладкой. На складных тростях коробка должна стоять выше первого сгиба, иначе трость не сложится.
- Окно для датчика: два цилиндра HC-SR04 должны выступать из передней стенки коробки; не ставь перед ними тонкий пластик или сетку — ультразвук через них не пройдёт.
- Дождь: просверли в дне коробки маленькое дренажное отверстие и положи внутрь пакетик силикагеля. Если дожди в твоих краях частые, вместо HC-SR04 возьми водонепроницаемый зонд JSN-SR04T.
- Кнопка: выбери большую кнопку с чётким щелчком и размести её там, где большой палец естественно лежит на рукоятке. Добавь вокруг неё бортик высотой 2 мм, чтобы её не нажимали случайно.
- Вкл/выкл: положение выключателя должно определяться на ощупь; писк при включении тоже подтверждает, что устройство работает.
- Разъём зарядки: USB-гнездо TP4056 должно быть доступно снизу коробки и закрываться резиновой заглушкой, когда не используется.
- Заметность: светоотражающая лента по бокам коробки помогает водителям заметить трость, когда пользователь переходит дорогу вечером.
Отправка координат с ESP32 + GPS
С Arduino трость во время SOS может оповестить звуком только тех, кто рядом. А с ESP32 и GPS-модулем координаты трости можно отправить близкому человеку сообщением. По умолчанию модуль NEO-6M выдаёт предложения NMEA на скорости 9600 бод, по одному определению координат в секунду; библиотека TinyGPSPlus разбирает их и отдаёт широту, долготу и число спутников. По даташиту первая фиксация после холодного старта занимает около 27 секунд; на практике на улице это от 30 секунд до нескольких минут, а в помещении фиксации часто нет вовсе. Маленькая резервная батарейка на модуле хранит последние данные спутников, поэтому при следующих включениях координаты появляются за несколько секунд. Подробности — в статье GPS-модуль NEO-6M.

Отправить сообщение можно двумя способами. Для школьной демонстрации проще всего подключить ESP32 к точке доступа на телефоне пользователя и отправлять сообщение через Telegram-бота. Для трости, которая по-настоящему работает на улице, лучше подходит GSM-модуль SIM800L, не зависящий от телефона: он отправляет SMS напрямую с SIM-карты внутри. SIM800L нужно 3,4–4,4 V, а при передаче он потребляет броски тока до 2 A, поэтому питай его прямо от ячейки 18650, а не от повышающего преобразователя, и поставь на его выводы питания конденсатор 1000 µF. Этот модуль работает только в сетях 2G; в странах, где 2G отключили, понадобится 4G-модуль вроде A7670 или SIM7600. Если хочется всё на одной плате, подойдёт 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}Код использует тот же конечный автомат, что и версия для Arduino; разница — в SOS. Как только начинается SOS, sendSms() по очереди отправляет SIM800L команды AT, AT+CMGF=1 для текстового режима и AT+CMGS="номер", ждёт приглашения «>», пишет сообщение и отправляет его по Ctrl+Z (26). Координаты приходят в сообщении ссылкой на карту, которая по нажатию на телефоне открывает приложение карт. Если GPS ещё не определил координаты, сообщение говорит об этом, а пока продолжается SOS, координаты отправляются заново каждые 5 минут. В тексте SMS намеренно нет турецких букв: символов вроде ş, ğ и ı нет в стандартном алфавите GSM, и на некоторых телефонах они отображаются искажёнными. Во время отправки код может ждать до 30 секунд; после включения SIM800L также нужно 10–30 секунд на регистрацию в сети.
Умная трость на Raspberry Pi Pico (MicroPython)
На Raspberry Pi Pico та же логика строится на MicroPython. time_pulse_us() работает как pulseIn() на Arduino и возвращает отрицательное значение, если эхо не пришло. Выходы ШИМ задаются 16-битной функцией duty_u16(); для вибромотора 65535 означает полную мощность. Шесть байтов MPU6050 читаются за один раз через i2c.readfrom_mem() и превращаются в знаковые числа вычитанием 65536 из значений больше 32767. Пины Pico тоже на 3,3 V, поэтому делитель на ECHO обязателен и здесь. При работе от батареи подавай выход 5 V с MT3608 на пин VSYS; HC-SR04 питается от той же шины 5 V. С Pico W, у которой есть Wi-Fi, состояние трости можно показывать и на веб-странице.
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)Советы для презентации на научной выставке или конкурсе
Умная трость — частый гость на научных выставках и школьных конкурсах (ещё больше идей — в нашей статье идеи проектов для научной выставки), поэтому жюри впечатляет не то, что трость вибрирует, а то, насколько хорошо определена проблема и насколько честно измерено решение. Самые сильные проекты начинаются с разговора с незрячим человеком или обществом незрячих: узнать от них, какие препятствия действительно мешают, что удобнее — вибрация или звук, и насколько тяжёлой может быть трость, — значит дать проекту и направление, и сильную историю.
- Проблема: белая трость не обнаруживает препятствия выше пояса, а лужи остаются незамеченными. Объясни это на конкретных примерах из интервью.
- Цель: разработать насадку, которая минимум за 1 секунду вибрацией сообщает о препятствиях на уровне тела и головы, почти не меняя вес трости и способ ею пользоваться.
- Метод: покажи выбор датчика (HC-SR04, JSN-SR04T, ToF) с обоснованием, угол установки с расчётом и сопоставление расстояния и вибрации с формулой.
- Результаты: представь результаты испытаний ниже в таблицах и графиках и включи случаи, когда что-то не сработало.
- Выводы и рекомендации: чётко сформулируй, что решает проект, где его границы (стекло, мягкие поверхности, дождь, обнаружение падения трости, а не человека) и каков следующий шаг.
| Испытание | Как проводить | Что записывать |
|---|---|---|
| Точность расстояния | По 10 измерений на расстоянии 30, 60, 100 и 150 см от стены | Средняя ошибка (см) и наибольшее отклонение |
| Типы препятствий | Стена, человек, столб 5 см, куст, стеклянная дверь, тканевая штора | Сколько раз из 10 обнаруживается каждое препятствие |
| Время реакции | Замедленное видео на телефон: появление препятствия и первая вибрация | Задержка в миллисекундах |
| Обнаружение воды | Опускать кончик в воду глубиной 0,5, 1, 2 и 3 см | С какой глубины срабатывает сигнал |
| Обнаружение падения | 20 падений и 100 постукиваний о землю | Число верных и ложных срабатываний |
| Время работы от батареи | Запустить с полной зарядки и дождаться отключения | Время в часах и конечное напряжение |
| Вес и баланс | Взвесить на кухонных весах, измерить расстояние от рукоятки | Добавленный вес (г) и момент (кг·см) |
Идеи для доработки
- Предупреждение о ступеньках и ямах: второй HC-SR04, направленный вниз и вперёд, обычно видит землю на постоянном расстоянии; если показание резко увеличилось, впереди ступенька вниз или яма.
- Голосовые сообщения: с помощью DFPlayer Mini вместо писка воспроизводи в наушнике записанные фразы вроде «впереди препятствие», «вода» или «батарея разряжена».
- Приложение для телефона: через Bluetooth BLE на ESP32 показывай уровень заряда и настройки на телефоне и меняй оттуда дальность.
- Направление: поставь два отдельных мотора слева и справа на рукоятке и показывай вибрацией, с какой стороны препятствие.
- Режим сна: отключай датчики, когда MPU6050 видит, что трость не двигается несколько минут; с глубоким сном ESP32 время работы от батареи вырастает в разы.
- Заметность ночью: с помощью фоторезистора (LDR) в темноте мигай светодиодом на кончике трости, чтобы водители издалека замечали пользователя.
Частые ошибки и как их исправить
| Симптом | Вероятная причина | Решение |
|---|---|---|
| Трость вибрирует без остановки | Датчик смотрит в землю или на саму трость | Увеличь угол установки и освободи пространство перед датчиком |
| Время от времени вибрация без причины | Случайное плохое эхо или другой источник ультразвука рядом | Медианный фильтр уже есть; при необходимости бери медиану 5 измерений |
| На больших расстояниях мотор не крутится | Значение ШИМ ниже порога запуска мотора | Держи минимальную силу выше 150 и закрепи мотор резиной |
| Arduino перезагружается при работе мотора | Ток мотора просаживает питание или нет диода | Используй готовый модуль и добавь на питание конденсатор 100 µF |
| MPU6050 всегда показывает 0 или одно и то же значение | Перепутаны SDA и SCL, адрес 0x69 или микросхема спит | Найди адрес I2C-сканером и проверь, что в регистр 0x6B записывается 0 |
| Ложные тревоги от постукиваний о землю | IMPACT_G низкий, а порог наклона близок к углу при ходьбе | Измерь наклон тестовым скетчем и подними LYING_DEG до 75 |
| Сигнал о воде не прекращается или не звучит | Неверный порог или окислившиеся дорожки | Заново измерь сухое и мокрое значения и при необходимости замени датчик |
| Зуммер молчит или всегда играет один и тот же тон | Установлен активный зуммер | Для tone() используй пассивный зуммер |
| SMS не отправляется | SIM800L не зарегистрирован в сети, слабое питание или на SIM-карте включён PIN-код | Проверь, что светодиод модуля мигает раз в 3 секунды, отключи PIN-код и питай модуль от 18650 |
Умная трость объединяет в одном проекте измерение расстояния, тактильную обратную связь, обнаружение событий датчиком движения и проектирование питания, а так как она отвечает реальной потребности, это один из самых осмысленных проектов на Arduino. Начни с базового кода из этого руководства, заполни таблицы испытаний, а затем по одному добавляй слои воды, падения и GPS. В приложении BlueGrays есть пошаговые проекты со схемами и готовым кодом: измерение расстояния на HC-SR04, парктроник, звук на зуммере и уведомления на ESP32, так что каждую часть трости можно сначала опробовать отдельно.
Частые вопросы
Как работает умная трость для незрячих?
Ультразвуковой датчик HC-SR04 у рукоятки посылает звуковые волны и вычисляет расстояние до препятствия впереди по времени возвращения эха. В зависимости от этого расстояния Arduino управляет вибромотором в рукоятке: чем ближе препятствие, тем чаще и сильнее вибрация. В более продвинутых версиях датчик воды на кончике замечает лужи, а MPU6050 определяет, что трость упала.
Какие компоненты нужны для умной трости на Arduino?
Для базовой версии хватит Arduino Nano, ультразвукового датчика HC-SR04, модуля вибромотора, пассивного зуммера и проводов-перемычек. В полной версии добавляются датчик уровня воды, MPU6050, кнопка, ячейка 18650 с модулем зарядки TP4056 и повышающим модулем MT3608. Для отправки координат нужны ESP32, GPS NEO-6M и GSM-модуль SIM800L.
На каком расстоянии умная трость обнаруживает препятствия?
HC-SR04 измеряет от 2 до 400 см, но на трости обычно ставят предел 150 см. Для человека, идущего со скоростью около 1 метра в секунду, это предупреждение за 1,5 секунды до препятствия. Если сильно увеличить дальность, в людных местах вибрация станет постоянной и пользователи начнут игнорировать сигналы.
Как умная трость понимает, что упала?
Акселерометр MPU6050 опрашивается 50 раз в секунду. Если после удара больше 2,5 g трость 2 секунды остаётся лежать ровно и неподвижно, считается, что она упала, и начинается писк для поиска. Если за 20 секунд трость не подняли и кнопку не нажали, звучит SOS; версия на ESP32 в этот момент отправляет координаты по SMS.
Сколько умная трость на Arduino работает от батареи?
В среднем схема потребляет около 50 mA. Одной ячейки 18650 на 2600 mAh с модулем зарядки TP4056 и повышающим модулем MT3608 хватает примерно на 25–30 часов, то есть около двух недель при использовании по два часа в день. Батарейки-«кроны» (PP3) на 9 V хватает лишь на 8–10 часов, и её не рекомендуют, потому что при работе мотора напряжение проседает.
Можно ли добавить в умную трость отправку координат по GPS?
Да. Если вместо Arduino взять ESP32 и добавить GPS-модуль NEO-6M, можно считывать широту и долготу трости. С GSM-модулем SIM800L во время SOS координаты уходят близкому человеку по SMS в виде ссылки на карту; если есть Wi-Fi, подойдёт и Telegram-бот. GPS работает на улице, но в помещении обычно не может определить координаты.
Что собрать после прочтения
Все проекты
Измерение расстояния HC-SR04 (вывод в порт)
Датчик HC-SR04 работает совсем как летучая мышь, которая находит дорогу в темноте: он посылает звук, которого ты не слышишь, слушает эхо и измеряет, насколько далеко предмет перед ним. Arduino каждые

Работа с зуммером
Интересно, как дверной звонок или сигнализация издают звук? Arduino заставляет маленький зуммер пищать через короткие промежутки: пик, пауза, пик, пауза. Ты видишь, как электричество превращается в зв

Настройка частоты зуммера
Ты управляешь звуком зуммера четырьмя кнопками: одна включает звук, другая выключает, а две оставшиеся делают его выше или ниже. Удерживай кнопку вверх или вниз, и высота звука меняется шаг за шагом,

Парктроник на HC-SR04
Мини-версия автомобильного парктроника: чем ближе предмет, тем чаще сигнал. Датчик расстояния измеряет предмет перед собой; если он дальше 20 сантиметров, горит зелёный светодиод, а если ближе — мигае

Ультразвуковой датчик расстояния HC-SR04 на ESP32
Ультразвуковой датчик HC-SR04 вычисляет расстояние по времени, за которое испущенная им звуковая волна отражается от предмета и возвращается (Time of Flight). В этом проекте ESP32 подаёт импульс 10 микросекунд на пин TRIG датчика, а длительность эха читает с пина ECHO через pulseIn(). Вычисленное расстояние в сантиметрах выводится на I2C LCD 16x2. HC-SR04 работает от 5V, поэтому на пине ECHO нужен делитель напряжения.

Парктроник на ESP32 (HC-SR04 + шкала из 5 светодиодов + зуммер + OLED)
ESP32-версия автомобильного парктроника! Ультразвуковой датчик HC-SR04 измеряет расстояние до предмета перед ним. По мере уменьшения расстояния шкала из 5 светодиодов заполняется (зелёный → жёлтый → красный), а интервал сигналов зуммера сокращается. Когда предмет совсем близко (меньше 10 см), звучит непрерывная тревога и горят все светодиоды. OLED SSD1306 показывает расстояние в см крупным шрифтом и сообщение состояния (STOP / CAUTION / CLOSE / STOP! / DANGER).

Робот, объезжающий препятствия
Как насчёт того, чтобы собрать умную машину, которая сама находит дорогу? Робот, объезжающий препятствия, постоянно осматривает всё вокруг благодаря ультразвуковому датчику спереди. Встретив препятств

Сигнализация движения на ESP32 (PIR + зуммер + светодиод + Telegram)
Умная охранная сигнализация, которая при обнаружении движения PIR-датчиком включает зуммер и светодиод и одновременно сразу отправляет владельцу уведомление в Telegram. В ней есть 30-секундная пауза, ограничивающая частоту повторных тревог; этот механизм не проверяет срабатывания PIR и не устраняет ложные тревоги. Подходит для дома, гаража, склада или мастерской. С ESP32-CAM можно добавить уведомления с фото, а с обычной ESP32 DevKit V1 приходят текстовые.
Другие статьи
Приложение BlueGrays
Собери эту схему шаг за шагом в телефоне
Схема подключения, код и список деталей на одном экране. 200+ проектов на Arduino, ESP32 и Pico готовы к сборке.
Наведи камеру телефона
Наведи камеру на код — откроется магазин приложений на телефоне.
или открыть магазин

