Руководства · 21 мин чтения

Умная трость на Arduino: оповещение о препятствиях, воде и падении

Как сделать умную трость для незрячих на Arduino: обнаружение препятствий на HC-SR04, вибросигнал, датчик воды, обнаружение падения на MPU6050, SOS и полный код.

Умная трость на Arduino предупреждает незрячего человека о препятствиях, которые не обнаружить белой тростью, и это один из самых популярных электронных проектов для школьных заданий и научных выставок. Ультразвуковой датчик HC-SR04, закреплённый у рукоятки, измеряет расстояние до того, что находится впереди; по мере приближения препятствия вибромотор в рукоятке срабатывает всё чаще и сильнее, а по желанию зуммер добавляет к каждому импульсу короткий писк. Так пользователь за несколько шагов чувствует препятствия выше пояса, которых кончик трости никогда не касается.

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

Ультразвуковой датчик расстояния HC-SR04 для умной трости
HC-SR04 посылает звуковую пачку 40 kHz и вычисляет расстояние по времени, за которое возвращается эхо. На умной трости он следит за препятствиями на расстоянии от 2 до 150 см внутри узкого конуса примерно в 15 градусов.

Что такое умная трость и что она делает?

Белая трость — инструмент, которому незрячий человек доверяет больше всего: каждое касание рассказывает о покрытии, бордюре, ступеньке или низком препятствии впереди. Но трость «видит» только то, чего касается её кончик. Препятствия выше пояса — низкая ветка дерева, вывеска над тротуаром, зеркало грузовика, открытое окно или кондиционер, торчащий из стены, — никогда не касаются кончика и вместо этого бьют по телу или голове. Умная трость создана, чтобы закрыть этот пробел: электронный датчик просматривает высоты и расстояния, недоступные трости, и передаёт результат в руку вибрацией.

Сразу проясним одну вещь: умная трость не заменяет белую трость и обучение ориентированию и мобильности, а дополняет их. У ультразвукового датчика есть слепые зоны, а батареи садятся, поэтому золотое правило конструкции такое: трость должна работать точно так же, как раньше, даже когда электроника полностью выключена. Международный день белой трости, который отмечают каждый год 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-принтере или небольшая коробка для проектов
Arduino Nano
Arduino Nano
Пассивный зуммер
Пассивный зуммер
Кнопка
Кнопка
Литий-ионный аккумулятор 18650
Литий-ионный аккумулятор 18650

Обнаружение препятствий: как измеряет 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-SR042–400 смДешёвый, простой, широкий конусНе водонепроницаемый, плохо видит мягкие поверхности
JSN-SR04TОколо 25–450 смВодонепроницаемый зонд, работает под дождёмНе видит ближе 25 см, дороже
VL53L1X (лазерный ToF)До 4 мМаленький, лёгкий, узкий лучНа солнце дальность падает, I2C на 3,3 V
Sharp GP2Y0A02 (инфракрасный)20–150 смАналоговый выход, простой кодОшибается на тёмных и блестящих поверхностях, потребляет около 33 mA
Знай слепые зоны ультразвукового датчика: мягкие поверхности — толстое пальто, кусты или штора — поглощают звук, а стеклянная дверь или наклонная панель отражают его в сторону, так что препятствие может вообще не появиться. Тонкие столбы толщиной 3–4 см тоже замечаются поздно, потому что внутри конуса они маленькие. Поэтому никогда не выдавай умную трость за единственное средство безопасности: это помощник, который работает вместе с белой тростью.

Частота вибрации и писка в зависимости от расстояния

Незрячий человек во многом воспринимает окружение на слух: приближающаяся машина, гул толпы, эхо постукивания трости от стены помогают ему находить дорогу. Зуммер, который пищит без остановки, скорее заглушает эти звуки, чем помогает. Поэтому главный канал оповещения в этом проекте — вибрация, и правило простое: чем ближе препятствие, тем чаще и сильнее виброимпульсы. Всё, что дальше 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Стоп
Модуль вибромотора для Arduino
Модуль вибромотора: рядом с плоским мотором-«таблеткой» диаметром около 10 мм стоят управляющий им транзистор и обратный диод. Пин IN подключается прямо к ШИМ-пину Arduino, а ток мотора берёт на себя модуль.

Чаще всего в тростях ставят плоский 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 / NanoESP32Raspberry Pi Pico
HC-SR04 VCC5VVIN (5 V)VBUS или шина 5 V
HC-SR04 TRIGD9GPIO 18GP2
HC-SR04 ECHOD10 (напрямую)GPIO 19 через делитель 1 kΩ / 2 kΩGP3 через делитель 1 kΩ / 2 kΩ
Вибромодуль IND5 (ШИМ)GPIO 25GP15
Пассивный зуммер (+)D8GPIO 26GP16
Кнопка (второй вывод на GND)D2GPIO 27GP14
Датчик воды +D7GPIO 33GP22
Датчик воды SA1GPIO 34 (ADC1)GP26 (ADC0)
MPU6050 SDAA4GPIO 21GP4
MPU6050 SCLA5GPIO 22GP5
MPU6050 VCC5V3V33V3
Контроль батареи (средняя точка делителя 100k/100k)A0GPIO 35 (ADC1)GP28 (ADC2)
GPS NEO-6M TX / RXНетGPIO 16 / GPIO 17Нет
SIM800L TXD / RXDНетGPIO 13 / GPIO 23Нет
Пин ECHO у HC-SR04 выдаёт импульс 5 V, а пины ESP32 и Pico работают от 3,3 V и 5 V не выдерживают. Собери делитель по схеме ECHO → 1 kΩ → провод к плате → 2 kΩ → GND (номиналы можно проверить в утилите делителя напряжения), и на пин придёт около 3,3 V. На ESP32 для аналоговых измерений используй пины ADC1 (GPIO 32–39), потому что ADC2 не работает при включённом Wi-Fi. На платах ESP32 с модулем WROVER пины GPIO 16 и 17 заняты PSRAM; если плата такая, перенеси GPS на два других пина. У всех модулей должна быть общая земля (GND).

Пошаговая сборка: начнём с базовой трости

  1. Сначала собери схему на макетной плате, а установку на трость оставь на самый конец. Вставь Arduino Nano в середину макетной платы и выведи 5V и GND на боковые шины.
  2. Подключи VCC и GND датчика HC-SR04 к шинам, TRIG — к D9, ECHO — к D10.
  3. Подключи VCC вибромодуля к 5V, GND — к GND, а пин IN (на некоторых модулях S) — к D5. Плюсовую ножку пассивного зуммера вставь в D8, а вторую — в GND.
  4. Загрузи базовый код ниже и открой монитор порта на скорости 9600 бод. Медленно приближай руку к датчику и отводи её; расстояние на экране должно меняться, а вибрация — ускоряться и замедляться вместе с ним.
  5. Держи датчик в 150, 100, 60 и 30 см от стены и сравни значение в мониторе порта с рулеткой. Если разница больше 1–2 см, проверь подключение и питание.
  6. Когда всё заработает, добавь датчик воды, MPU6050 и кнопку и переходи к полному коду.
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}

В коде нигде нет длинного 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% времени. Как он работает, рассказано в статье датчик уровня воды, а чем отличается от платы дождя — в статье датчик дождя.

Датчик уровня воды для Arduino
Датчик уровня воды: когда вода замыкает чередующиеся медные дорожки, напряжение на пине S растёт. На умной трости он стоит на 1–2 см выше кончика и получает питание только в момент измерения.
Покрой верхнюю часть датчика без дорожек и его разъём термоклеем или лаком для ногтей: вода должна касаться только чувствительных дорожек. Не оставляй кабель болтаться снаружи трости: протяни его внутри алюминиевой трости или обмотай вокруг древка в термоусадочной трубке. Более прочный вариант — два винта из нержавеющей стали в 1 см друг от друга чуть выше кончика: один идёт на D7, другой на A1, который подтянут к GND резистором 100 kΩ. Когда вода замыкает винты, A1 растёт; код остаётся тем же, нужно только заново измерить WATER_LIMIT.

Обнаружение падения на 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.

Модуль акселерометра и гироскопа MPU6050 (GY-521)
MPU6050 (GY-521): трёхосевой акселерометр и гироскоп. Он закреплён внутри коробки так, что стрелка X, нанесённая на плату, идёт вдоль древка трости.

При каждом измерении код вычисляет две величины. Первая — полное ускорение: 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 g2 сЕсли условие нарушено — назад в WALK, если выполняется — ALARM
ALARM (тревога)Писк и вибрация каждую секунду, чтобы трость можно было найти20 сОтменяется нажатием кнопки или подъёмом трости (наклон меньше 60°)
SOS... --- ... азбукой Морзе звуком, светодиодом и вибрациейДо отменыКороткое нажатие останавливает; ESP32 отправляет SMS

Такая последовательность отсекает большинство ложных срабатываний. Постукивание тростью о землю при ходьбе тоже может давать короткие удары больше 2,5 g, но через секунду трость всё ещё в руке и наклонена на 40–55 градусов, поэтому этап проверки сразу отменяется; пользователь даже не заметит. А вот когда трость падает и остаётся лежать, тревога начинается примерно через 3 секунды, и писк каждую секунду помогает пользователю, у которого выпала трость, найти её на слух. Если трость поднять, тревога прекращается сама. Если 20 секунд никто не реагирует, код считает, что это может быть настоящее падение, и запускает SOS. Удержание кнопки 2 секунды тоже в любой момент запускает SOS вручную.

Этот метод определяет падение трости, а не человека. Если при падении трость остаётся в руке или в итоге прислонена к стене, тревога не сработает; поэтому всегда оставляй кнопку ручного SOS и чётко объясни это ограничение в презентации. Чтобы напрямую измерять падение человека, датчик должен быть в отдельном устройстве на поясе или запястье.

Код для тестирования и калибровки

Пороги немного отличаются для каждой трости, каждой поверхности и каждого датчика. Прежде чем переходить к основному коду, загрузи скетч ниже: каждую четверть секунды он выводит в монитор порта расстояние, сырое значение датчика воды, максимальное ускорение за последнюю четверть секунды и наклон. Если записывать значения в таблицу, заодно заполнится раздел результатов в отчёте по проекту.

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. Измерь датчик воды сухим и с концом, опущенным на 1 см в стакан воды. Если сухой он показывает, например, 0–20, а мокрый 520, выбери для WATER_LIMIT середину — 270–300.
  2. Держи трость обычным хватом и води ею из стороны в сторону, как при ходьбе. Запиши наклон: обычно он держится между 40 и 55 градусами и должен быть меньше PICKUP_DEG (60).
  3. Положи трость на пол. Наклон должен показывать 85–90, а ускорение — около 0,95–1,05 g; это подтверждает LYING_DEG (70) и окно 0,8–1,2 g.
  4. Постучи тростью о землю, как обычно при ходьбе, и следи за пиковыми значениями g. Затем 10 раз урони трость на коврик. Установи IMPACT_G ниже значений при падении; даже если постукивания превысят этот порог, их отфильтрует проверка наклона.
  5. Сделай по 10 измерений на расстоянии 30, 60, 100 и 150 см от стены и сравни среднее с рулеткой.

Полный код: препятствия, вода, падение, SOS и предупреждение о разряде батареи

В полном коде четыре задачи работают в одном цикле, не мешая друг другу, а обнаружение падения — это небольшой конечный автомат на enum и switch-case. У кнопки три задачи: при ходьбе короткое нажатие включает или выключает писк при препятствиях (вибрация остаётся включённой всегда), во время тревоги или SOS короткое нажатие отменяет их, а долгое нажатие на 2 секунды в любой момент запускает SOS. Высокий писк означает «звук включён», низкий — «звук выключен».

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}
Функция или параметрЧто делает?
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), на VIN45 г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 г и большеНесколько днейТяжело; большинство пауэрбанков отключаются при малом токе
Модуль зарядки литий-ионных аккумуляторов TP4056
Модуль зарядки TP4056: заряжает ячейку 18650 от USB до 4,2 V током 1 A. Версия с микросхемой защиты DW01 также защищает ячейку от переразряда и короткого замыкания; нагрузка подключается к OUT+ и OUT−.

Рекомендуемая цепь питания такая: ячейка 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.

Выход MT3608 можно поднять подстроечным резистором вплоть до 28 V, а с завода модуль может прийти настроенным на любое значение. Перед подключением к Nano измерь выход мультиметром и крути подстроечный резистор, пока не будет 5,0 V; неправильно настроенный повышающий преобразователь убивает плату за секунды. Если во время тестов плата питается только от USB, A0 остаётся «висящим», показания батареи случайны, и может звучать писк разряда; это нормально.

Корпус, вес и эргономика

Большинство складных белых тростей весят 200–300 г, и пользователь часами водит ими из стороны в сторону движением запястья. Каждый добавленный грамм умножается на расстояние от руки и нагружает запястье. Коробка 150 г в 10 см ниже рукоятки даёт на запястье дополнительный момент 150 × 10 = 1,5 кг·см; та же коробка на 60 см ниже дала бы 9 кг·см, в шесть раз больше. Поэтому собери всю электронику в одной коробке и размести её сразу под рукояткой; вниз до кончика идут только датчик воды и его тонкий кабель. Старайся, чтобы весь добавленный вес был меньше 150 г.

ДетальПримерный вес
Arduino Nano7 г
HC-SR049 г
MPU6050 (GY-521)2 г
Вибромодуль + пассивный зуммер5 г
Датчик уровня воды и кабель8 г
Ячейка 1865045–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.

GPS-модуль NEO-6M с керамической антенной
GPS-модуль NEO-6M: керамической антенне нужен вид на небо, поэтому она стоит наверху коробки, подальше от металлических деталей. Скорость по умолчанию — 9600 бод.

Отправить сообщение можно двумя способами. Для школьной демонстрации проще всего подключить 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).

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}

Код использует тот же конечный автомат, что и версия для 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, состояние трости можно показывать и на веб-странице.

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)

Советы для презентации на научной выставке или конкурсе

Умная трость — частый гость на научных выставках и школьных конкурсах (ещё больше идей — в нашей статье идеи проектов для научной выставки), поэтому жюри впечатляет не то, что трость вибрирует, а то, насколько хорошо определена проблема и насколько честно измерено решение. Самые сильные проекты начинаются с разговора с незрячим человеком или обществом незрячих: узнать от них, какие препятствия действительно мешают, что удобнее — вибрация или звук, и насколько тяжёлой может быть трость, — значит дать проекту и направление, и сильную историю.

  1. Проблема: белая трость не обнаруживает препятствия выше пояса, а лужи остаются незамеченными. Объясни это на конкретных примерах из интервью.
  2. Цель: разработать насадку, которая минимум за 1 секунду вибрацией сообщает о препятствиях на уровне тела и головы, почти не меняя вес трости и способ ею пользоваться.
  3. Метод: покажи выбор датчика (HC-SR04, JSN-SR04T, ToF) с обоснованием, угол установки с расчётом и сопоставление расстояния и вибрации с формулой.
  4. Результаты: представь результаты испытаний ниже в таблицах и графиках и включи случаи, когда что-то не сработало.
  5. Выводы и рекомендации: чётко сформулируй, что решает проект, где его границы (стекло, мягкие поверхности, дождь, обнаружение падения трости, а не человека) и каков следующий шаг.
ИспытаниеКак проводитьЧто записывать
Точность расстоянияПо 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
При поиске неисправностей проверяй части по одной: сначала тестовым скетчем убедись, что расстояние, значение воды и наклон выглядят разумно, а потом загружай полный код. Если MPU6050 не отвечает, его адрес найдёт скетч-сканер из статьи Что такое I2C. Если плата перезагружается, когда начинается вибрация, причина почти всегда в питании; статья Arduino постоянно перезагружается шаг за шагом решает эту проблему.

Умная трость объединяет в одном проекте измерение расстояния, тактильную обратную связь, обнаружение событий датчиком движения и проектирование питания, а так как она отвечает реальной потребности, это один из самых осмысленных проектов на 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 (вывод в порт)
#27Новичок

Измерение расстояния HC-SR04 (вывод в порт)

Датчик HC-SR04 работает совсем как летучая мышь, которая находит дорогу в темноте: он посылает звук, которого ты не слышишь, слушает эхо и измеряет, насколько далеко предмет перед ним. Arduino каждые

4.6(46)
71
Работа с зуммером
#13Новичок

Работа с зуммером

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

4.7(89)
161
Настройка частоты зуммера
#14Средний

Настройка частоты зуммера

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

4.6(69)
118
Парктроник на HC-SR04
#28Средний
Premium

Парктроник на HC-SR04

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

4.8(64)
85
Ультразвуковой датчик расстояния HC-SR04 на ESP32
#116Средний

Ультразвуковой датчик расстояния HC-SR04 на ESP32

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

4.9(28)
45
Парктроник на ESP32 (HC-SR04 + шкала из 5 светодиодов + зуммер + OLED)
#142Продвинутый
Premium

Парктроник на ESP32 (HC-SR04 + шкала из 5 светодиодов + зуммер + OLED)

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

0.0(0)
14
Робот, объезжающий препятствия
#81Продвинутый
Premium

Робот, объезжающий препятствия

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

5.0(38)
49
Сигнализация движения на ESP32 (PIR + зуммер + светодиод + Telegram)
#145Продвинутый
Premium

Сигнализация движения на ESP32 (PIR + зуммер + светодиод + Telegram)

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

0.0(0)
9