Скан-зависание (stop-and-scan) — карта в помещении без GPS

Скан-зависание — самый дешёвый способ построить карту окружения для маленького дрона, у которого нет ни GPS, ни 2D-лидара, ни вычислителя под визуальный SLAM. Идея простая: дрон зависает на одной точке, серво крутит однолучевой дальномер по горизонту, на каждом шаге снимается одно расстояние. Получается веер расстояний 180°. Перелетев на следующую точку и повторив, можно по нескольким веерам собрать сетку занятости (occupancy grid) или 2D-карту локального окружения.

Цикл скан-зависания в Gazebo: серво крутится, /scan/status переходит SCANNING → COMPLETE → STOPPED

Зачем нужно

Когда дрон летает без GPS внутри помещения, у него нет источника абсолютной локализации. Барометр дрейфует, оптический поток ловит низкочастотный снос, IMU интегрирует ошибку. Чтобы построить карту препятствий и хотя бы локально привязаться к ней, нужен датчик дальности.

«Правильное» решение — 2D-лидар вроде RPLidar A1: сканирует 360° непрерывно, 5–10 раз в секунду, отдаёт LaserScan напрямую. Минусы: вес 170 граммов, цена ~100 USD, отдельное питание.

«Дешёвое» решение — однолучевой дальномер на серво. Вес 15 граммов вместе с серво, цена ~35 USD, питание от того же 5-вольтового шины. Минус — скан не непрерывный: дрон должен зависнуть на ~22 секунды, пока серво проходит весь горизонт.

Скан-зависание — это компромисс: жертвуем частотой скана ради массы, цены и сложности. Для разведки в помещении без GPS на учебном дроне — оправдано.

Как устроен цикл

  1. Зависание. Дрон входит в режим стабилизации высоты и положения. На квадрокоптерах с ArduPilot это обычно LOITER или ALT_HOLD плюс ручная подкрутка по позиции с барометра/оптотока. Стабильность критична — если дрон уплывает на 20 см за время скана, веер расстояний оказывается размазан по нескольким позам.
  2. Команда /drone/sweep/start. Хост-нода sweep_node принимает пустое сообщение и инициирует проход.
  3. Шаг по углу. Серво двигается на 1° (от 0° к 180°), хост ждёт выдержку 120 мс — за это время серво доезжает и стабилизируется, плюс приходит свежий замер с дальномера.
  4. Запись сэмпла. Текущее расстояние от дальномера складывается в массив. Параллельно публикуется /scan/status: SCANNING и /drone/sweep/progress (доля прохода 0.0–1.0).
  5. Конец прохода. На 181-м замере (углы 0°, 1°, 2°, …, 180°) собирается единый sensor_msgs/LaserScan и публикуется в /drone/sweep/result. Статус переключается на COMPLETE.
  6. Кулдаун. Внешняя нода autoscan_node ждёт ~10 секунд (даёт дрону восстановить позицию) и снова посылает /drone/sweep/start.
  7. Остановка по команде. Если приходит /drone/sweep/stop, текущий проход дорабатывается, новый не стартует. Статус — STOPPED. Команда /drone/sweep/resume снимает блокировку.

Полный цикл — около 22 секунд скана плюс 10 секунд кулдауна. Один полный веер каждые ~30 секунд.

Что на выходе

Веер из 181 числа: расстояние до препятствия по азимутам 0°, 1°, …, 180° относительно носа дрона. В формате ROS 2 это sensor_msgs/LaserScan со следующими полями:

  • angle_min = 0, angle_max = π, angle_increment = π/180
  • ranges[181] — массив расстояний в метрах
  • range_min = 0.2, range_max = 8.0 (характеристики TF-Luna)
  • frame_id = iris_claudedrone/sg90_arm/tf_luna_sweep — координатная цепочка через TF, луч уже привязан к углу серво

Дальше веер обычно превращают в локальную сетку занятости: по каждому лучу клеточки до точки попадания помечаются как «свободно», конечная клеточка — как «занято», за ней — «неизвестно». При нескольких пройденных позах сетки накладываются, и появляется грубая 2D-карта окружения.

Ограничения

  • Время. 22 секунды на проход — это вечность для динамической сцены. Если в помещении что-то двигается (человек прошёл), полупроход видит его, второй полупроход — нет. Карта получается противоречивой.
  • Стабильность зависания. Нужен либо очень хороший контур стабилизации, либо опорные сенсоры (оптический поток, лазерный высотомер). Без этого веер размазан.
  • Один уровень. Скан только в горизонтальной плоскости на текущей высоте дрона. Препятствия выше или ниже линии скана не видны. Двухосевой подвес (pan + tilt) расширяет до 3D, но удваивает время.
  • Минимальное расстояние сенсора. TF-Luna слепнет ближе 0.2 метра, VL53L0X — ближе 5 см. Стены вплотную дрон не видит.
  • Узкий луч. 2° угол обзора у TF-Luna — между точками скана 1° есть мёртвые зоны. Тонкие провода, тросы могут ускользнуть.
  • Дискретный шаг. Шаг 1° — компромисс между разрешением и временем. Уменьшение шага кратно увеличивает время прохода.

Альтернативы

ПодходСкоростьЦенаКогда лучше
2D-лидар (RPLidar A1)5–10 Гц~100 USDподвижные сцены, динамичный полёт
Визуальный SLAM (RealSense T265)30–60 Гц~200 USDесть текстура, нужна 6DoF поза
Cartographer / slam_toolboxнепрерывнотребует 2D-лидаранужна согласованная глобальная карта
Стереокамера + глубинная сеть10–30 Гц~50–150 USDбортовой GPU есть, нужна детальная сцена
Скан-зависание (это статья)0.05 Гц~35 USDминимум массы и цены, статичная сцена

Как используется в проекте claudeDrone

В нашей симуляции скан-зависание реализовано связкой SG90 + TF-Luna на iris_claudedrone (TASK-001 и TASK-002). Конечный автомат имеет четыре состояния — IDLE (ждёт /drone/sweep/start), SCANNING (идёт проход), COMPLETE (проход завершён, результат опубликован), STOPPED (остановлен по команде). Состояния транслируются в топик /scan/status (std_msgs/String) — внешний код видит, идёт ли сейчас скан, без подписки на сами данные.

Тестировалось в Gazebo на сцене indoor_room.sdf с параметрами полёта indoor.parm (GPS_TYPE=0, AHRS_EKF_TYPE=10): дрон взлетал на 1.5 метра по LOITER режиму через MAVROS, после стабилизации запускал autoscan_node и проходил несколько последовательных скан-циклов с командами STOP/RESUME между ними. Записи и снимки — в ~/drone_media/sim/task002-status/.

Текущая реализация делает скан только по азимуту. Для следующей итерации обсуждается двухосевой подвес (добавить pan-серво) — даст 3D-облако точек, но удвоит время прохода и потребует второго sweep_node.