search icon search icon ВЕРСИЯ ДЛЯ СЛАБОВИДЯЩИХ

Инженерный тур. 2 этап

Участникам заключительного этапа предстоит разработать систему автоматизированного мониторинга объектов топливно-энергетического комплекса при помощи квадрокоптера.

Чтобы успешно решить данную задачу необходимо:

  • знать и понимать принципы устройства и работы квадрокоптера, принципы базовой и программной настройки квадрокоптера;
  • обладать навыками 3D-моделирования, программирования, работы с компьютерным зрением и машинным обучением.

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

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

Командное задание позволяет участникам апробировать свои решения в симуляторе: инженерам — настроить квадрокоптер и подготовить виртуальный мир, а программистам — написать программный код для автономного полета квадрокоптера. Для успешного выполнения заданий второго отборочного этапа участникам необходимо правильно распределить задачи, а также наладить систему планирования и коммуникации внутри команды.

Компетенции, необходимые участникам для решения задач второго этапа:

  • базовые навыки работы с летающими робототехническими системами;
  • программирование (Python);
  • навыки работы с компьютерным зрением (OpenCV);
  • 3D-моделирование;
  • Front-end разработка.
Индивидуальные задачи

Индивидуальные задачи второго этапа инженерного тура открыты для решения. Соревнование доступно на платформе Яндекс.Контест: https://contest.yandex.ru/contest/69899/enter/.

Задача 1.1.(8 баллов)
Сборка квадрокоптера
Тема: базовые навыки работы с летающими робототехническими системами

Условие

Ознакомьтесь с видео и отметьте все электронные компоненты и части коптера, которые были подключены или установлены неправильно (ссылка на видео: https://disk.yandex.ru/i/0AeXVnc6tzUWHQ).

  1. Подключение шлейфа радиоприемника к полетному контроллеру.
  2. Подключение шлейфа радиоприемника к радиоприемнику.
  3. Подключение питания к полетному контроллеру.
  4. Подключение сигнального провода регуляторов оборота от мотора № 1 к полетному контроллеру.
  5. Подключение сигнального провода регуляторов оборота от мотора № 2 к полетному контроллеру.
  6. Подключение сигнального провода регуляторов оборота от мотора № 3 к полетному контроллеру.
  7. Подключение сигнального провода регуляторов оборота от мотора № 4 к полетному контроллеру.
  8. Подключение силовых проводов от регулятора оборотов мотора № 1 к плате распределения питания.
  9. Подключение силовых проводов от регулятора оборотов мотора № 2 к плате распределения питания.
  10. Подключение силовых проводов от регулятора оборотов мотора № 3 к плате распределения питания.
  11. Подключение силовых проводов от регулятора оборотов мотора № 4 к плате распределения питания.
  12. Подключение питания светодиодной ленты.
  13. Подключение сигнального провода светодиодной ленты к Raspberry Pi.
  14. Подключение питания (5V) к Raspberry Pi.
  15. Подключение лазерного дальномера к Raspberry Pi.
  16. Подключение шлейфа от камеры к Raspberry Pi.
  17. Подключение проводной связи от полетного контроллера к Raspberry Pi.
  18. Установка пропеллера на мотор № 1.
  19. Установка пропеллера на мотор № 2.
  20. Установка пропеллера на мотор № 3.
  21. Установка пропеллера на мотор № 4.

Решение

Для решения задачи необходимо внимательно посмотреть видео по сборке квадрокоптера из образовательного курса: https://stepik.org/lesson/1431188/step/7?unit=1449603 или ознакомиться с документацией по сборке квадрокоптера: https://clover.coex.tech/ru/assemble_4_2_ws.

Ответ

1, 3, 4, 5, 14, 15, 16, 17, 18, 20, 21.

Задача 1.2.(5 баллов)
Настройка квадрокоптера
Тема: базовые навыки работы с летающими робототехническими системами

Условие

Ознакомьтесь с видео и отметьте все ошибки, допущенные при настройке квадрокоптера (ссылка на видео: https://disk.yandex.ru/i/CqE4B18KRF-JMw).

  1. При загрузке прошивки в полетный контроллер.
  2. В выборе конфигурации рамы квадрокоптера.
  3. При калибровке компаса.
  4. При калибровке гироскопа.
  5. При калибровке акселерометра.
  6. При калибровке уровня горизонта.
  7. При установке ориентации полетного контроллера.
  8. При калибровке радиоаппаратуры управления.
  9. При настройке режимов полетного_ контроллера.
  10. При назначении аварийного отключения моторов.
  11. При настройке параметров питания.

Решение

Для решения задачи необходимо внимательно посмотреть видео по сборке квадрокоптера из образовательного курса: https://stepik.org/lesson/1431188/step/7?unit=1449603 или ознакомиться с документацией по сборке квадрокоптера: https://clover.coex.tech/ru/assemble_4_2_ws.

Ответ

7, 8, 10, 11.

Задача 1.3.(6 баллов)
Автономный полет. Настройка образа
Темы: настройка квадрокоптера, базовые навыки работы с летающими робототехническими системами

Условие

Ознакомьтесь с видео и отметьте все ошибки, допущенные при настройке образа квадрокоптера (ссылка на видео: https://disk.yandex.ru/i/Qm5kl5WyMX6jhg).

  1. Настройка параметра aruco.
  2. Настройка параметра aruco_detect.
  3. Настройка параметра aruco_map.
  4. Настройка параметра aruco_vpe.
  5. Настройка параметра map.
  6. Настройка размера aruco-маркера по умолчанию.
  7. Настройка параметра direction_z.
  8. Настройка параметра direction_y.
  9. Настройка параметров карты: длина маркера.
  10. Настройка параметров карты: количество маркеров по оси \(X\).
  11. Настройка параметров карты: количество маркеров по оси \(Y\).
  12. Настройка параметров карты: расстояние между центрами меток по оси \(X\).
  13. Настройка параметров карты: расстояние между центрами меток по оси \(Y\).
  14. Настройка параметров карты: номер первого маркера.
  15. Настройка параметров карты: «ключ» начала нумерации при генерации карты.
  16. Перезагрузка пакетов клевера на образе.

Примечания

Сборка квадрокоптера является стандартной.

Решение

Для решения задачи необходимо внимательно посмотреть видео по сборке квадрокоптера из образовательного курса: https://stepik.org/lesson/1431188/step/7?unit=1449603 или ознакомиться с документацией по сборке квадрокоптера: https://clover.coex.tech/ru/assemble_4_2_ws.

Ответ

5, 7, 8, 9, 15.

Задача 1.4.(5 баллов)
PID-регулятор. Настройка коптера
Темы: настройка квадрокоптера, базовые навыки работы с летающими робототехническими системами

Условие

Ознакомьтесь с видео и по характеру полета квадрокоптера соотнесите их с подобранными коэффициентами PID-регулятора.

Ссылки на видео:

  1. https://disk.yandex.ru/d/uj6JXzlr7uSDFQ/01пид.mp4.
  2. https://disk.yandex.ru/d/uj6JXzlr7uSDFQ/02пид.mp4.
  3. https://disk.yandex.ru/d/uj6JXzlr7uSDFQ/03пид.mp4.
  4. https://disk.yandex.ru/d/uj6JXzlr7uSDFQ/04пид.mp4.

Примечания

Сборка квадрокоптера Clever 4 является стандартной. Для демонстрации менялся один из коэффициентов относительно рекомендованного значения в прошивке PX4 полетного контроллера.
  1. MC_PITCHRATE_P = 0.087
    MC_PITCHRATE_I = 0.037
    MC_PITCHRATE_D = 0.0044
    MC_PITCH_P = 8.5
    MC_ROLLRATE_P = 0.015
    MC_ROLLRATE_I = 0.037
    MC_ROLLRATE_D = 0.0044
  2. MC_PITCHRATE_P = 0.087
    MC_PITCHRATE_I = 0.037
    MC_PITCHRATE_D = 0.0044
    MC_PITCH_P = 8.5
    MC_ROLLRATE_P = 0.087
    MC_ROLLRATE_I = 0.037
    MC_ROLLRATE_D = 0.010
  3. MC_PITCHRATE_P = 0.300
    MC_PITCHRATE_I = 0.037
    MC_PITCHRATE_D = 0.0044
    MC_PITCH_P = 8.5
    MC_ROLLRATE_P = 0.087
    MC_ROLLRATE_I = 0.037
    MC_ROLLRATE_D = 0.0044
  4. MC_PITCHRATE_P = 0.020
    MC_PITCHRATE_I = 0.037
    MC_PITCHRATE_D = 0.0044
    MC_PITCH_P = 8.5
    MC_ROLLRATE_P = 0.087
    MC_ROLLRATE_I = 0.037
    MC_ROLLRATE_D = 0.0044

Решение

Для решения задачи необходимо внимательно посмотреть видео по сборке квадрокоптера из образовательного курса: https://stepik.org/lesson/1431188/step/7?unit=1449603 или ознакомиться с документацией по настройке квадрокоптера Clover: https://clover.coex.tech/ru/setup.html#усредненные-коэффициенты-pid-для-клевера-4.

Ответ

1 — D, 2 — C, 3 — A, 4 — B.

Задача 1.5.(10 баллов)
Пакуем сумки
Темы: ROS, Linux, системы координат

Условие

Вы удаленно работаете инженером-программистом БПЛА в компании, которая занимается мониторингом объектов топливно-энергетического комплекса (ТЭК).

Для выполнения мониторинга был создан квадрокоптер, работающий на платформе ROS, с использованием образа Clover: https://clover.coex.tech/ru/image.html.

Коллеги, находящиеся за километры от вас на объекте ТЭК, столкнулись с проблемой. Во время последней миссии квадрокоптер патрулировал участок трубопровода, чтобы обнаружить потенциальные утечки и повреждения. В процессе выполнения задачи дрон несколько раз отклонялся от намеченной траектории.

Для того чтобы провести отладку и исправить баг, существуют два варианта: паковать сумки и ехать к коллегам, или просить их паковать rosbag файл, который позволяет сохранять данные, публикуемые в топики ROS. Конечно, вы, как уважающий себя удаленщик, выбрали второй метод.

На этом этапе НТО выполните более простую задачу — используя rosbag файл, восстановите миссию, по которой должен был пролететь дрон.

rosbag файл: https://disk.yandex.ru/d/sVxcVxwBsyXf-w.

Файл начал записываться после взлета квадрокоптера и закончил после его посадки и дизарма. Восстановите navigate и land команды, выполненные в полете (не учитывая взлет).

Каждая команда navigate была выполнена с frame_id="aruco_map". Поэтому координаты в восстановленных командах должны быть в системе координат aruco_map.

Формат входных данных

Миссия задается псевдокодом с командами

navigate(x: float, y: float, z: float)

Для простоты опустим все параметры из оригинальной команды, кроме x, y, z, но примем frame_id="aruco_map" и land().

Формат выходных данных

В ответе укажите набор команд (каждая на своей строке), которым следовал дрон.

В команде navigate указывайте имена параметров и передавайте числа, округленные до десятых (включая .0 для целых чисел).

Пример указания команды navigate: navigate(x=1.0, y=1.0, z=2.0).

Решение

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

Для начала ознакомимся с имеющимися в rosbag файле топиками. Для этого воспользуемся библиотекой bagpy: https://github.com/jmscslgroup/bagpy.
Python
from bagpy import bagreader

bag = bagreader("drone_flight.bag")
for t in bag.topics:
   print(t)

Получим такой список:

/mavros/altitude
/mavros/battery
/mavros/estimator_status
/mavros/extended_state
/mavros/imu/data
/mavros/imu/data_raw
/mavros/imu/mag
/mavros/imu/static_pressure
/mavros/imu/temperature_imu
/mavros/local_position/odom
/mavros/local_position/pose
/mavros/local_position/velocity_body
/mavros/local_position/velocity_local
/mavros/px4flow/ground_distance
/mavros/px4flow/raw/optical_flow_rad
/mavros/px4flow/raw/send
/mavros/px4flow/temperature
/mavros/rc/out
/mavros/setpoint_position/local
/mavros/setpoint_raw/target_attitude
/mavros/setpoint_raw/target_local
/mavros/state
/mavros/time_reference
/mavros/timesync_status
/mavros/vision_pose/pose
/tf
/tf2_web_republisher/status
/tf_static

Понимаем, что были записаны не все топики, однако достаточно имеющихся топиков, связанных с setpoint (низкоуровневое задание точки полета квадрокоптера) и tf (системы координат и их преобразования).

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

Этот способ более прост для понимания участниками Олимпиады, однако требует установленного ROS или виртуальной машины Clover.

Для начала запустим в терминале roscore.

После этого в другом терминале запустим rosbag play drone_flight.bag, тем самым начав передачу данных в топики, которые были записаны в BAG-файле.

Для визуализации данных в топике запустим rviz и через кнопку Add добавим отображения TF (рис. 1.1).

Рис. 1.1. Добавление отображения

После добавления увидим все системы координат, которые существовали на момент записи BAG-файла (рис. 1.2).

Рис. 1.2. Системы координат в BAG-файле

Здесь можно увидеть, что начало системы координат navigate_target указывает на координаты точки, в которую сейчас летит дрон с использованием navigate.

После этого напишем простой код, который регистрирует изменения преобразований системы координат navigate_target относительно aruco_map. Смещение СК navigate_target относительно aruco_map будет показывать координаты точки в СК aruco_map, в которую летит дрон.

Python
import rospy
import tf2_ros

rospy.init_node("rosbag_solution")

tfBuffer = tf2_ros.Buffer()
tf2_ros.TransformListener(tfBuffer)

rate = rospy.Rate(1)
last_coords = None
while not rospy.is_shutdown():
    transform = tfBuffer.lookup_transform(
        target_frame="aruco_map",
        source_frame="navigate_target",
        time=rospy.Time(),
        timeout=rospy.Duration(10),
    )
    coords = (
        round(transform.transform.translation.x, 2),
        round(transform.transform.translation.y, 2),
        round(transform.transform.translation.z, 2),
    )
    if coords != last_coords:
        last_coords = coords
        print(last_coords)
    rate.sleep()

Запустим код, следом за ним запустим воспроизведение BAG-файла. Получаем следующий результат (рис. 1.3).

Рис. 1.3. Запуск кода

Первую точку откинем, так как это координата предыдущей точки полета дрона (команда navigate была выполнена до начала записи BAG-файла, что противоречит условию).

Остается подставить полученные координаты под формат ответа, не забыв про land().

Ответ

navigate(x=1.0, y=1.0, z=1.0)
navigate(x=5.5, y=2.0, z=2.0)
navigate(x=9.0, y=6.0, z=1.5)
navigate(x=5.0, y=5.0, z=2.5)
land()
Задача 1.6.(7 баллов)
Летаем оптимально
Темы: математика, кратчайший путь, представление цвета

Условие

После исправления ошибки, возникшей на дроне, необходимо провести мониторинг участка магистрального нефтепровода.

Магистральный нефтепровод — это сложная инженерная система, включающая в себя линейную часть, головные и промежуточные перекачивающие станции, резервуарные парки и другие сооружения. Каждому типу сооружений присвоен определенный цвет.

Напишите ROS-ноду, которая выполняет следующие действия:

  • взлет квадрокоптера;
  • включение светодиодной ленты заданного цвета, коды цветов: https://gist.github.com/ntomaterials/60d0d05a6e1b47e1c6edb87fa328e011;
  • облет по точкам, в которых находятся сооружения заданного цвета;
  • посадка в конечной точке маршрута.

В качестве ответа создайте программу, генерирующую псевдокод ROS-ноды (см. формат выходных данных), который будет выполнять задание наиболее оптимально (кратчайшим путем).

В случае, если оптимальных решений несколько, выведите любое из них.

Формат входных данных

Первая строка содержит координаты старта \((x_{start}, y_{start})\) через пробел; вторая строка содержит координаты финиша \((x_{finish}, y_{finish})\) через пробел.

Далее поочередно идут \(1 \leqslant N \leqslant 1000\) строк, содержащие параметры каждой метки: \(color_i\) — наименование цвета \(i\)-й метки; \(x_i\), \(y_i\) — координаты расположения \(i\)-й метки.

\(0 \leqslant x_{start}, y_{start}, x_{finish}, y_{finish}, x_i, y_i < 100.\)

В конце ввода идет строка, содержащая \(color_{req}\) — наименование цвета маркеров, по которым необходимо выполнить полет.

Формат выходных данных

Псевдокод для квадрокоптера, содержащий только функции:

  • navigate(x: int, y: int, auto_arm: bool = False)
  • set_effect(r: int, g: int, b: int)
  • land()

Тестовые данные

Номер тестаСтандартный вводСтандартный вывод
1
71 36
8 18
Red 28 46
Red 13 39
Red 12 45
SlateBlue 12 13
SlateBlue 75 41
SlateBlue 0 1
Red 78 89
SlateBlue
navigate(x=71, y=36, auto_arm=true)
set_effect(r=255, g=0, b=0)
navigate(x=75, y=41)
navigate(x=12, y=13)
navigate(x=0, y=1)
navigate(x=8, y=18)
land()

Решение

  1. Зажигаем светодиодную ленту соответствующим цветом.
  2. Зная координаты необходимых меток, составляем все вариации полета со старта и до финиша через функцию permutations из библиотеки itertools, при этом пролетая все метки.
  3. Рассчитываем дистанцию полета от одной метки к другой: \[distance = \sqrt{(x_b-x_a)^2+(y_b-y_a)^2}.\]
  4. Также не забываем о том, что до первого маркера необходимо долететь со старта, а с последнего — на финиш. Получаем расстояния каждой возможной вариации и берем наименьшую из всех.

Ниже представлено решение на языке Python.

Python
from collections import defaultdict
from itertools import permutations

COLORS = {...}  # коды цветов из условия


def dist(p1, p2):
    return ((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) ** 0.5


def hex_to_rgb(hex: str) -> tuple[int, int, int]:
    return tuple(int(hex.replace("#", "")[i : i + 2], 16) for i in (0, 2, 4))


start = tuple(map(int, input().split()))
finish = tuple(map(int, input().split()))

colors = defaultdict(list)

while len(m := input().split()) == 3:
    color, *coords = m
    colors[color].append(list(map(int, coords)))

points = colors[m[0]]
led_command = "set_effect(r={}, g={}, b={})".format(*hex_to_rgb(COLORS[m[0]]))

min_perm = []
if points:
    min_perm = min(
        permutations(points),
        key=lambda perm: sum(dist(perm[i], perm[i + 1]) for i in range(len(perm) - 1))
        + dist(start, perm[0])
        + dist(perm[-1], finish),
    )

ans = (
    [led_command, "navigate(x={}, y={}, auto_arm=true)".format(*start)]
    + ["navigate(x={}, y={})".format(*e) for e in min_perm]
    + ["navigate(x={}, y={})".format(*finish), "land()"]
)

print("\n".join(ans))
Задача 1.7.(8 баллов)
Сколько перекрестков?
Темы: OpenCV, контуры

Условие

Для анализа инфраструктуры на одном из участков, оборудованном сложной сетью трубопроводов, был использован дрон, который проводил воздушную съемку и фиксировал состояние труб. Обработайте полученное изображение, чтобы определить количество пересечений трубопроводов на участке. Данная информация будет полезна для планирования обслуживания и оценки износа труб в местах перекрестков.

Изображение представляет собой черно-белое фото, где дороги выделены белыми линиями на черном фоне. Гарантируется, что в изображении присутствуют только белые и черные пиксели; помимо этого гарантируется, что пересекаться может не больше двух дорог.

Формат входных данных

По пути ./input.jpg будет находиться файл с входным изображением, которое необходимо обработать.

Формат выходных данных

Одно число — количество перекрестков белых линий.

Тестовые данные

Номер тестаСтандартный вводСтандартный вывод
1
3
2
4

Решение

  1. Загрузка и подготовка изображения:

    1. Программа открывает файл с изображением (input.png) и переводит его в черно-белый формат. Это значит, что каждый пиксель становится либо белым (значение 1), либо черным (значение 0).
    2. Изображение превращается в массив чисел (с помощью библиотеки numpy), чтобы его можно было легко обрабатывать программно.
  2. Выделение областей с пересечениями (с помощью свертки):

    • Программа использует специальную маленькую матрицу, которую называют «ядром» (kernel). Это матрица размером \(3\times 3\): \[\begin{matrix} 1 &1 &1\\ 1 &5 &1\\ 1 &1 &1 \end{matrix}\]
    • Идея в том, чтобы для каждого пикселя изображения посчитать сумму значений его соседей с такими коэффициентами. Если в этой области пересекаются линии (то есть вокруг пикселя много белых точек), то сумма будет высокой.
    • Функция convolve (из библиотеки scipy) «пробегается» по всему изображению и для каждого пикселя считает такую сумму. Получается новое изображение, где значение пикселя говорит о том, сколько вокруг него белых пикселей с учетом коэффициентов ядра.
  3. Нахождение точек, где могут быть пересечения:

    • После свертки программа ищет все пиксели, у которых полученное значение больше или равно 8. Почему 8? Подобрано экспериментально.
    • Таким образом находятся все потенциальные точки пересечения.
  4. Группировка близко расположенных точек (кластеризация):

    • Из-за того как работает свертка, одно реальное пересечение может быть представлено несколькими соседними пикселями с высоким значением. Но в данном случае нужно посчитать каждое пересечение только один раз.
    • Для этого используется алгоритм DBSCAN (из библиотеки scikit-learn). Он группирует (кластеризует) близко расположенные точки вместе.
    • Каждая группа точек (кластер) считается одним пересечением. То есть количество таких кластеров и будет искомым числом перекрестков.
  5. Вывод результата:

    • После группировки программа считает количество получившихся кластеров.
    • Это число выводится как результат работы программы — количество пересечений труб (перекрестков белых линий).

Ниже представлено решение на языке Python.

Python
from PIL import Image
import numpy as np
from scipy.ndimage import convolve
from sklearn.cluster import DBSCAN


def count_intersections(image_path):
   image = Image.open(image_path).convert("1")
   image_array = np.array(image)

   kernel = np.array([
                      [1, 1, 1],
                      [1, 5, 1],
                      [1, 1, 1],
                      ])
  
   convolved = convolve(image_array.astype(np.int32), kernel, mode='constant', cval=0)
  
   im = convolved * (255 / convolved.max())
   im = im.astype(np.uint8)

   intersection_points = np.where(convolved >= 8)

   intersection_points = list(zip(*intersection_points))
   if not intersection_points:
       return 0
  
   clustering = DBSCAN(eps=10, min_samples=2).fit(intersection_points)
   n_clusters = len(set(clustering.labels_)) - (1 if -1 in clustering.labels_ else 0)

   return n_clusters

if __name__ == "__main__":
   print(count_intersections("input.png"))
Задача 1.8.(5 баллов)
Проектирование фланца
Темы: квадрокоптер, расчет

Условие

После неудачной посадки квадрокоптера повредилась деталь крепления луча. Для крепления луча к корпусу квадрокоптера используется специализированный фланец. Для изготовления нового фланца необходима 3D-модель, но на производстве есть только чертеж данной детали. Спроектируйте фланец в CAD-программе, используя чертеж. Для контроля соответствия детали определите массу получившейся детали в CAD-программе.

Материал — алюминий (плотность: 2800 кг/м\(^3\)).

Чертеж фланца см. на рис. 1.4.

Рис. 1.4. Чертеж фланца

Решение

Необходимо спроектировать фланец по чертежу в CAD-программе, задать материал и взвесить модель при помощи анализа модели в CAD-программе.

Ответ

1435.

Задача 1.9.(5 баллов)
Конструкторская документация
Тема: конструкторская документация

Условие

Тест на знание конструкторской документации, 1 балл за каждый верный ответ.

Ответьте на вопросы:

  1. Какие документы КД относится к текстовым?

    1. Чертеж детали.
    2. Перечень элементов.
    3. Схема.
    4. Спецификация.
  2. Какое обозначение радиуса правильное?

  3. Какая поверхность имеет наибольшую шероховатость?

  4. Сечение A–A соответствует изображению...

  5. Укажите, где правильно соединен вид и разрез.

Решение

  1. Согласно ГОСТ 2.102-2013 из четырех вариантов ответа к текстовой документации относится только перечень элементов (б) и спецификация (г).

    Правильный вариант ответа: б, г.

  2. Согласно ГОСТ 2.102-2013 обозначение радиуса допускается только большой буквой R и численным обозначением, следующим сразу за буквой R.

    Правильный вариант ответа: а.

  3. Согласно ГОСТ 2.102-2013 шероховатость тем меньше чем меньше число ее обозначающее.

    Правильный вариант ответа: б.

  4. Согласно ГОСТ 2.102-2013 разрез — это изображение, полученное при мысленном рассечении предмета плоскостью.

    Правильный вариант ответа: в.

  5. Согласно ГОСТ 2.102-2013 правильно соединен вид и разрез только на второй картинке.

    Правильный вариант ответа: б.

Ответ

1 — б, г, 2 — а, 3 — б, 4 — в, 5 — б.

Командные задачи

Командные задачи второго этапа инженерного тура открыты для решения. Соревнование доступно на платформе Яндекс.Контест: https://contest.yandex.ru/contest/69921/enter/.

Задача 2.1.(15 баллов)
Расчет луча квадрокоптера
Темы: квадрокоптер, сопротивление материалов

Условие

Необходимо рассчитать на прочность луч квадрокоптера.

На рис. 2.1 показано сечение луча и нагрузки, действующие на него.

Рассчитайте:

  1. момент инерции сечения луча, мм\(^4\) (округлить до сотых);
  2. момент сопротивления сечения луча, мм\(^3\) (округлить до сотых);
  3. максимальные нормальные напряжения луча, МПа (округлить до тысячных);
  4. максимальные касательные напряжения луча, МПа (округлить до тысячных);
  5. максимальные главные напряжения луча, МПа (округлить до тысячных).

Рис. 2.1. Луч квадрокоптера

Решение

Так как сечение является квадратным, значения по оси \(X\) и \(Y\) равны.

  1. Расчет момента инерции сечения.

    Сечение луча состоит из квадратов, формула для расчета момента инерции квадрата \[I_z=\frac{h^4}{12},\] где \(h\) — сторона квадрата.

    • Расчет момента инерции внешнего квадрата \[I_1=\frac{h^4}{12}=\frac{25^4}{12}=32552{,08}~\text{мм}^4.\]
    • Расчет внутренних вырезов \[I_z=I_c+md^2,\] \[I_2=4\cdot \left(\frac{h^4}{12}+h^2\cdot 5{,}75\right)=4\cdot \left(\frac{9{,}5^4}{12}+9{,}5^2\cdot 5{,}75\right)=14650{,58} ~\text{мм}^4,\] где \(h=9{,}5\) мм — сторона вырезанного квадрата, \(m=5{,}75\) мм — расстояние между центром большого квадрата и маленьких квадратов.
    • Расчет всего сечения.

      Вычитаем из момента инерции большого квадрата моменты инерции вырезов \[I_{x,y}=I_1-I_2=32552{,08}-14650{,58}=17901{,49}~\text{мм}^4.\] Момент инерции сечения: \[I_{x,y}=17901{,49}~\text{мм}^4.\]

  2. Расчет момента сопротивления сечения.

    Максимальный момент сопротивления сечения считается находится в точке максимального удаления от оси, то есть \[\frac{y}{2}=\frac{h}{2}=12{,}5~\text{мм}.\]

    Формула для расчета сопротивления сечения квадрата: \[W_xy=\frac{I_{x,y}}{\frac{y}{2}}=\frac{17901{,49}}{12{,}5}=1432{,12}~\text{мм}^3.\]

    Момент сопротивления сечения: \(W_{xy}=1432{,12}~\text{мм}^3\).

    Расчет максимальных напряжений.

    Формула для расчета нормальных напряжений в точке (формула Навье – Стокса): \[\sigma=\frac{M_z\cdot y}{I}.\]

    Формула для расчета максимальных нормальных напряжений: \[\sigma_{max}=\frac{M_z}{W}.\]

    Формула для расчета касательных напряжений (формула Журавского): \[\tau=\frac{Q\cdot S_z}{I_x\cdot B_y}.\]

    Построим эпюру нагружения, рис. 2.2.

    Рис. 2.2. Эпюра нагружения

  3. Расчет максимальных нормальных напряжений.

    Подставляем значения в формулу: \[\sigma_{\text{max}} = \frac{M_z}{W} = \frac{5000}{1432{,12}} = 34{,}913 ~\text{МПа}.\]

    Максимальные нормальные напряжения: \[\sigma_{\text{max}} = 34{,}913 ~\text{МПа}.\]

  4. Расчет максимальных касательных напряжений.

    Для нахождения максимальных касательных напряжений необходимо посчитать касательные напряжения в четырех точках и построить эпюру.

    Рис. 2.3. Касательные напряжения

    1. Расчет касательных напряжений в точке 1.

      Формула для расчета касательных напряжений: \[\tau = \frac{Q \cdot S_z}{I_x \cdot B_y}.\]

      Рассчитываем площадь отсеченной части сечения, которая находится выше точки 1. В данном случае это значение равно 0: \[A_{\text{отс1}} = 0.\]

      Рассчитываем центр тяжести отсеченной части, в данном случае центр тяжести находится в точке 1: \[y_{c1} = \frac{h}{2} = \frac{25}{2} = 12{,}5 ~ \text{мм}.\]

      Статический момент отсеченной части: \[S_x = y_{c1} \cdot A_{\text{отс1}} = 12{,}5 \cdot 0 = 0 ~ \text{мм}^3.\]

      Считаем касательные напряжения: \[\tau_{\text{т.1}} = \frac{Q \cdot S_z}{I_x \cdot B_y} = \frac{100 \cdot 0}{17901{,49} \cdot 25} = 0 ~\text{МПа},\]

      где \(B_y = 25 ~ \text{мм}\) — ширина сечения.

    2. Расчет касательных напряжений в точке 2.

      Формула для расчета касательных напряжений: \[\tau = \frac{Q \cdot S_z}{I_x \cdot B_y}.\]

      Рассчитываем площадь отсеченной части сечения, которая находится выше точки 2: \[A_{\text{отс2}} = h \cdot t = 25 \cdot 2 = 50 ~ \text{мм}^2,\]

      где \(t = 2 ~ \text{мм}\) — высота сечения.

      Рассчитываем центр тяжести отсеченной части: \[y_{c2} = \frac{h}{2} - \frac{t}{2} = \frac{25}{2} - \frac{2}{2} = 11{,}5 ~ \text{мм}.\]

      Статический момент отсеченной части: \[S_x = y_{c2} \cdot A_{\text{отс2}} = 11{,}5 \cdot 50 = 575 ~ \text{мм}^3.\]

      Так как в точке 2 происходит изменение ширины сечения, посчитаем значение касательных напряжений до и после изменения: \[B_y^1 = 25 ~ \text{мм},\] \[B_y^2 = 6 ~ \text{мм}.\]

      Считаем касательные напряжения в точке 1: \[\tau_{\text{т.2}}^1 = \frac{Q \cdot S_z}{I_x \cdot B_y^1} = \frac{100 \cdot 575}{17901{,49} \cdot 25} = 0{,}128 ~\text{МПа}.\]

      Считаем касательные напряжения в точке 2: \[\tau_{\text{т.2}}^2 = \frac{Q \cdot S_z}{I_x \cdot B_y^2} = \frac{100 \cdot 575}{17901{,49} \cdot 6} = 0{,}535 ~\text{МПа}.\]

    3. Расчет касательных напряжений в точке 3.

      Формула для расчета касательных напряжений: \[\tau = \frac{Q \cdot S_z}{I_x \cdot B_y}.\]

      Рассчитываем площадь отсеченной части сечения, которая находится выше точки 3: \[A_{\text{отс3}} = 3(b \cdot t) + (h \cdot t) = 3 \cdot (9{,}5 \cdot 2) + (25 \cdot 2) = 107 ~ \text{мм}^2,\]

      где \(t = 2 ~ \text{мм}\) — высота сечения, \(b = 9{,}5 ~ \text{мм}\) — высота выреза.

      Рассчитываем центр тяжести отсеченной части. Так как отсеченная фигура является сложной, воспользуемся формулой расчета центра тяжести сложных фигур: \[y_{c3}=\frac{\sum A_i \cdot x_i}{\sum A_i} ,\] где \(\sum A_i\) — площадь простой фигуры, \(x_i\) — расстояния от центра масс простой фигуры до начала системы координат.

      \[y_{c3} = \frac{\sum A_i \cdot x_i}{\sum A_i} = \frac{(25 \cdot 2) \cdot 11{,}5 + (9{,}5 \cdot 2 \cdot 3) \cdot 5{,}75}{(25 \cdot 2) + (9{,}5 \cdot 2 \cdot 3)} = 8{,43692} ~ \text{мм}.\]

      Статический момент отсеченной части: \[S_x = y_{c3} \cdot A_{\text{отс3}} = 8{,43692} \cdot 107 = 902{,}75 ~ \text{мм}^3.\]

      Так как в точке 3 происходит изменение ширины сечения, посчитаем значение касательных напряжений до и после изменения: \[B_y^1 = 6 ~ \text{мм},\] \[B_y^2 = 25 ~ \text{мм}.\]

      Считаем касательные напряжения в точке 1: \[\tau_{\text{т.3}}^1 = \frac{Q \cdot S_z}{I_x \cdot B_y^1} = \frac{100 \cdot 902{,}75}{17901{,49} \cdot 6} = 0{,}8404 ~\text{МПа}.\]

      Считаем касательные напряжения в точке 2: \[\tau_{\text{т.3}}^2 = \frac{Q \cdot S_z}{I_x \cdot B_y^2} = \frac{100 \cdot 902{,}75}{17901{,49} \cdot 25} = 0{,}2017 ~\text{МПа}.\]

    4. Расчет касательных напряжений в точке 4.

      Формула для расчета касательных напряжений: \[\tau = \frac{Q \cdot S_z}{I_x \cdot B_y}.\]

      Рассчитываем площадь отсеченной части сечения, которая находится выше точки 4: \[A_{\text{отс4}} = 3(b \cdot t) + (h \cdot t) + \left(h\cdot\frac{ t}{2}\right) = 3 \cdot (9{,}5 \cdot 2) + (25 \cdot 2) + \left(25\cdot \frac{ 2}{2}\right) = 132 ~ \text{мм}^2,\]

      где \(t = 2 ~ \text{мм}\) — высота сечения, \(b = 9{,}5 ~ \text{мм}\) — высота выреза.

      Рассчитываем центр тяжести отсеченной части. Так как отсеченная фигура является сложной, воспользуемся формулой расчета центра тяжести сложных фигур: \[y_{c4}=\frac{\sum A_i \cdot x_i}{\sum A_i} ,\] где \(\sum A_i\) — площадь простой фигуры, \(x_i\) — расстояния от центра масс простой фигуры до начала системы координат. \[\begin{aligned} y_{c4} = \frac{\sum A_i \cdot x_i}{\sum A_i} = \frac{(25 \cdot 2) \cdot 11,5 + (9,5 \cdot 2 \cdot 3) \cdot 5,75 + \left(25 \cdot\frac{ 2}{2}\right) \cdot 0,5}{(25 \cdot 2) + (9,5 \cdot 2 \cdot 3) + \left(25\cdot \frac{ 2}{2}\right)} =\\ {}= 6,9337 ~ \text{мм}. \end{aligned}\]

      Статический момент отсеченной части: \[S_x = y_{c4} \cdot A_{\text{отс4}} = 6{,}9337 \cdot 132 = 915{,}25 ~ \text{мм}^3.\]

      Считаем касательные напряжения: \[\tau_{\text{т.4}} = \frac{Q \cdot S_z}{I_x \cdot B_y^1} = \frac{100 \cdot 915{,}25}{17901{,49} \cdot 25} = 0{,}2045 ~\text{МПа}.\]

    5. Эпюра касательных напряжений

      Рис. 2.4. Эпюра касательных напряжений

      Из этого следует, максимальные касательные напряжения: \[\tau_{\text{max}} = 0{,}8404 ~\text{МПа}.\]

  5. Расчет максимальных главных напряжений

    Общая формула расчета главных напряжений: \[\sigma_{\text{max}} = \frac{\sigma}{2} + \frac{1}{2} \sqrt{\sigma^2 + 4\tau^4}.\]

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

    Максимальные главные напряжения: \[\sigma_{\text{max}} = 34{,}913 ~\text{МПа}.\]

Ответ

17901,49; 1432,12; 34,913; 0,84; 34,913.

Задача 2.2.(30 баллов)
Почти как на финале
Темы: ROS, Gazebo, визуализация, Front-end

Условие

Легенда

Для мониторинга угольного завода используются квадрокоптеры на базе ROS. Крыша у каждого строения, расположенного на территории завода, имеет свой цвет и определяется его назначением:

  • административные здания имеют красные крыши,
  • лаборатории — зеленые,
  • вход в шахту — желтый,
  • здания для обогащения угля — синие.

Разработайте систему, которая позволит операторам эффективно управлять работой дронов на заводе. Представьте работу системы перед тем, как ее использование будет одобрено на реальном заводе.

Задание

  1. Установить образ симулятора Gazebo или ПО на свою систему.
  2. Подготовить bash/python скрипт для настройки образа симулятора (включение навигации по aruco-картам и т. д.).

    1. Настройка clover.launch.
    2. Настройка aruco.launch\verb.
    3. Дополнительные настройки, необходимые для выполнения задания.
  3. Подготовить bash/python скрипт для случайной генерации мира.

    1. Использовать эту модель https://github.com/bart02/dronepoint в качестве зданий.
    2. Установить здания в количестве 5 шт.
    3. Минимальное расстояние между центрами зданий 1 м.
    4. Каждое здание должно находиться в пределах \(x \in [0{,}9]\), \(y \in [0{,}9]\).
    5. Координаты и цвет каждого здания генерируются случайно.
  4. Подготовить python скрипт (ROS-ноду) для выполнения полета и выполнения сканирования.

    1. При сканировании определить координаты здания в системе aruco_map и цвет его крыши.
    2. На миссию ограничено количество взлетов и посадок — 1.
    3. Возврат на старт после выполнения сканирования.
    4. Все результаты сканирования записывать в топик (название /buildings, тип данных на усмотрение команды).
  5. Разработать веб-приложение с графическим интерфейсом пользователя, в котором будут находиться кнопки управления миссией (старт, пауза, стоп), найденные здания с привязкой к карте.

    1. Кнопки управления миссией:

      1. старт — запуск кода с предыдущего шага (если код запущен — кнопка недоступна);
      2. стоп — прерывание кода, выполнение посадки;
      3. kill switch — прерывание кода, дизарм дрона.
    2. Автоматически обновляемая 2D-карта с возможностью отображения начала координат и каждого найденного здания (квадрат с учетом цвета их крыши).
    3. Автоматически обновляемый список найденных зданий с отображением цвета, координат, типа (административные здания имеют красные крыши, лаборатории — зеленые, вход в шахту — желтый, здания для обогащения угля — синие).

Критерии оценивания

Критерий Балл за 1 случай Количество случаев Сумма
1 Установка образа симулятора Gazebo или ПО на систему 2 1 2
2 Подготовка bash/python скрипта для настройки симулятора
2.1 Настройка clover.launch 1 1 1
2.2 Настройка aruco.launch 1 1 1
3 Подготовка bash/python скрипта для случайной генерации мира
3.1 Использование модели зданий из репозитория https://github.com/bart02/dronepoint 1 1 1
3.2 Установка зданий в количестве 5 шт. 0,4 5 2
3.3 Минимальное расстояние между центрами зданий 1 м 1 1 1
3.4 Каждое здание находится в пределах \(x \in [0{,}9]\), \(y \in [0{,}9]\) 0,2 5 1
3.5 Случайная генерация координат и цвета каждого здания 1 1 1
4 Подготовка ROS-ноды для выполнения полета и выполнения сканирования
4.1 Сканирование зданий с определением координат и цвета крыши 0,5 10 5
4.2 Ограничение на количество взлетов и посадок — 1 2 1 2
4.3 Возврат на старт после выполнения сканирования 2 1 2
4.4 Запись результатов сканирования в топик (/buildings) 2 1 2
5 Разработка веб-приложения с графическим интерфейсом для управления миссией
5.1 Реализация кнопок управления миссией (старт, пауза, стоп, kill switch) 1 3 3
5.2 Автоматически обновляемая 2D-карта с координатами и цветом зданий 3 1 3
5.3 Список найденных зданий с отображением цвета, координат и типа здания 3 1 3
ИТОГО 30

Решение

Генерация карты. Код программы на языке Python:
Python
import os
import shutil
import sys

# Карта ArUco в виде строки
aruco_map = '''# id length  x   y   z   rot_z   rot_y   rot_x
0   0.22    0.0 0.0 0   0   0   0
1   0.22    1.0 0.0 0   0   0   0
2   0.22    2.0 0.0 0   0   0   0
3   0.22    3.0 0.0 0   0   0   0
4   0.22    4.0 0.0 0   0   0   0
5   0.22    5.0 0.0 0   0   0   0
6   0.22    6.0 0.0 0   0   0   0
7   0.22    7.0 0.0 0   0   0   0
8   0.22    8.0 0.0 0   0   0   0
9   0.22    9.0 0.0 0   0   0   0
10  0.22    0.0 1.0 0   0   0   0
...
99  0.22    9.0 9.0 0   0   0   0
'''

def replace_string(file_path, old_string, new_string):
    """
    Заменяет строку в файле.

    Аргументы:
        file_path (str): Путь к файлу, который нужно изменить.
        old_string (str): Строка, которую нужно найти в файле.
        new_string (str): Строка, на которую нужно заменить найденную строку.
    """
    with open(file_path, 'r') as file:
        lines = file.readlines()

    for index, line in enumerate(lines):
        if old_string in line:
            lines[index] = new_string + '\n'  # Заменяем строку
            break

    with open(file_path, 'w') as file:
        file.writelines(lines)  # Записываем изменения в файл

# Определение имени пользователя и пути
if len(sys.argv) > 1:
    if len(sys.argv) > 2:
        directory_name = ' '.join(sys.argv[1:])
    else:
        directory_name = sys.argv[1]
    user_name = directory_name.split('/')[0]
else:
    directory_name = 'clover/Desktop'
    user_name = 'clover'

# Сохраняем карту ArUco в файл
with open('Nto.txt', 'w') as file:
    file.write(aruco_map)

# Проверка и копирование карты ArUco
if 'Nto.txt' not in os.listdir(f"/home/{user_name}/catkin_ws/src/clover/aruco_pose/map"):
    shutil.copy('Nto.txt', f"/home/{user_name}/catkin_ws/src/clover/aruco_pose/map")

# Проверка наличия моделей в директории .gazebo
if 'models' in os.listdir(f"/home/{user_name}/.gazebo/"):
    for model in os.listdir(f"/home/{user_name}/.gazebo/models"):
        # Удаляем ненужные модели
        if model in ['aruco_Nto_txt', 'dronepoint_blue', 'dronepoint_green', 'dronepoint_red', 'dronepoint_yellow']:
            shutil.rmtree(f"/home/{user_name}/.gazebo/models/{model}")
            continue

    # Копирование моделей в директорию .gazebo
    for model_folder in os.listdir(f"/home/{directory_name}/II_stage_KZ/models"):
        shutil.copytree(f'/home/{directory_name}/II_stage_KZ/models/{model_folder}', f"/home/{user_name}/.gazebo/models/{model_folder}", dirs_exist_ok=True)
else:
    shutil.copytree(f'/home/{directory_name}/II_stage_KZ/models', f"/home/{user_name}/.gazebo/models")

# Изменение строк в launch файле для запуска ArUco
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/aruco.launch', '<arg name="aruco_detect"', '    <arg name="aruco_detect" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/aruco.launch', '<arg name="aruco_map"', '    <arg name="aruco_map" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/aruco.launch', '<arg name="aruco_vpe"', '    <arg name="aruco_vpe" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/aruco.launch', '<arg name="placement"', '    <arg name="placement" default="floor"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/aruco.launch', '<arg name="length"', '    <arg name="length" default="0.22"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/aruco.launch', '<arg name="map"', '    <arg name="map" default="Nto.txt"/>')

# Изменение строк в launch файле для запуска Clover
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="fcu_conn"', '    <arg name="fcu_conn" default="usb"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="fcu_ip"', '    <arg name="fcu_ip" default="127.0.0.1"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="fcu_sys_id"', '    <arg name="fcu_sys_id" default="1"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="gcs_bridge"', '    <arg name="gcs_bridge" default="tcp"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="web_video_server"', '    <arg name="web_video_server" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="rosbridge"', '    <arg name="rosbridge" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="main_camera"', '    <arg name="main_camera" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="optical_flow"', '    <arg name="optical_flow" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="aruco"', '    <arg name="aruco" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="rangefinder_vl53l1x"', '    <arg name="rangefinder_vl53l1x" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="led"', '    <arg name="led" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="blocks"', '    <arg name="blocks" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="rc"', '    <arg name="rc" default="false"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="force_init"', '    <arg name="force_init" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="simulator"', '    <arg name="simulator" default="false"/>')

# Изменение строк в launch файле для симулятора
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="type"', '    <arg name="type" default="gazebo"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="mav_id"', '    <arg name="mav_id" default="0"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="est"', '    <arg name="est" default="ekf2"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="vehicle"', '    <arg name="vehicle" default="clover"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="main_camera"', '    <arg name="main_camera" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="maintain_camera_rate"', '    <arg name="maintain_camera_rate" default="false"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="rangefinder"', '    <arg name="rangefinder" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="led"', '    <arg name="led" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="gps"', '    <arg name="gps" default="false"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="use_clover_physics"', '    <arg name="use_clover_physics" default="false"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="gui"', '    <arg name="gui" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="world_name" value="$(find clover_simulation)', '        <arg name="world_name" value="$(find clover_simulation)/resources/worlds/Nto.world"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="verbose"', '        <arg name="verbose" value="true"/>')
Код полета. Программа на языке Python:
Python
import rospy
import cv2
import math
import os
import sys
import pickle
import numpy as np
from sensor_msgs.msg import Image, CameraInfo
from std_msgs.msg import String
from geometry_msgs.msg import PointStamped, Point
from cv_bridge import CvBridge
from clover import srv
from std_srvs.srv import Trigger
import tf2_ros
import tf2_geometry_msgs
import image_geometry
from pymavlink import mavutil
from mavros_msgs.srv import CommandLong
from mavros_msgs.msg import State

# Инициализация ROS-ноды
rospy.init_node('cv', disable_signals=True)

# Прокси-сервисы
get_telemetry = rospy.ServiceProxy('get_telemetry', srv.GetTelemetry)
navigate = rospy.ServiceProxy('navigate', srv.Navigate)
set_attitude = rospy.ServiceProxy('set_attitude', srv.SetAttitude)
land = rospy.ServiceProxy('land', Trigger)
send_command = rospy.ServiceProxy('mavros/cmd/command', CommandLong)

# Публикаторы
buildings_pub = rospy.Publisher('/buildings', String, queue_size=1)

# Настройка TF2
tf_buffer = tf2_ros.Buffer()
tf_listener = tf2_ros.TransformListener(tf_buffer)
camera_model = image_geometry.PinholeCameraModel()
camera_model.fromCameraInfo(rospy.wait_for_message('main_camera/camera_info', CameraInfo))

# Мост для конвертации изображений OpenCV
bridge = CvBridge()

# Цветовые коды и названия моделей зданий
color_text = {
    'red': '\033[31m\033[1m',
    'green': '\033[32m\033[1m',
    'yellow': '\033[33m\033[1m',
    'cyan': '\033[36m\033[1m',
    'white': '\033[37m\033[1m',
    'blue': '\033[34m\033[1m'
}

colors = {
    'red': (0, 0, 255),
    'green': (0, 255, 0),
    'yellow': (0, 255, 255),
    'blue': (255, 0, 0)
}

model_name = {
    'red': 'Найдено административное здание',
    'green': 'Найдена лаборатория',
    'yellow': 'Найден вход в шахту',
    'blue': 'Найдено здание для обогащения угля'
}

# Инициализация переменных
buildings = ''
count_models = 1
length = 86.7
opr_models = {1: [], 2: [], 3: [], 4: [], 5: []}
img_map = None

# Попытка загрузить изображение карты
try:
    img_map = cv2.imread('static/image.png')
except FileNotFoundError:
    pass


def real_round(number: float, poz: int = 0) -> float:
    """
    Округляет число с плавающей запятой до заданной точности.
    
    Аргументы:
        number (float): Число для округления.
        poz (int): Количество знаков после запятой.
    
    Возвращает:
        float: Округленное число.
    """
    a = number * 10 * 10 ** poz
    return math.floor(a / 10) / 10 ** poz if a % 10 < 5 else math.ceil(a / 10) / 10 ** poz


def check_position(x: float, y: float, color: str) -> bool:
    """
    Проверяет, является ли позиция уникальной (не слишком близкой к уже найденным моделям).

    Аргументы:
        x (float): Координата X.
        y (float): Координата Y.
        color (str): Цвет объекта.

    Возвращает:
        bool: True, если позиция уникальна, иначе False.
    """
    models = filter(lambda model: len(model) == 3 and model[2] == color, opr_models.values())
    for model in models:
        x0, y0, _ = model
        if (x0 - x) ** 2 + (y0 - y) ** 2 <= 1:
            return False
    return True


def land_wait():
    """
    Ожидает, пока дрон не приземлится и не будет разармен.
    """
    land()
    while get_telemetry().armed:
        rospy.sleep(0.2)
    rospy.loginfo(f'{color_text["yellow"]}[DISARM]{color_text["white"]}')


def navigate_wait(x: float = 0, y: float = 0, z: float = 0, yaw: float = float('nan'), speed: float = 0.6,
                  frame_id: str = 'aruco_map', auto_arm: bool = False, tolerance: float = 0.2):
    """
    Навигация дрона в указанную точку и ожидание достижения цели.

    Аргументы:
        x (float): Координата X цели.
        y (float): Координата Y цели.
        z (float): Координата Z (высота).
        yaw (float): Угол поворота дрона.
        speed (float): Скорость дрона.
        frame_id (str): Система координат.
        auto_arm (bool): Нужно ли армировать дрон.
        tolerance (float): Допустимое отклонение от цели.
    """
    navigate(x=x, y=y, z=z, yaw=yaw, speed=speed, frame_id=frame_id, auto_arm=auto_arm)
    if auto_arm:
        rospy.loginfo(f'{color_text["yellow"]}[ARM]{color_text["white"]}')
        rospy.loginfo(f'Takeoff by {color_text["green"]}Body{color_text["white"]} Z = {z}')
    else:
        rospy.loginfo(f'Flight to {color_text["cyan"]}{frame_id.capitalize()}{color_text["white"]}')

    while not rospy.is_shutdown():
        try:
            telem = get_telemetry(frame_id='navigate_target')
        except rospy.ServiceException:
            continue

        if math.sqrt(telem.x ** 2 + telem.y ** 2 + telem.z ** 2) < tolerance:
            break
        rospy.sleep(0.2)


def img_xy_to_point(xy: tuple, dist: float) -> Point:
    """
    Преобразует координаты пикселей на изображении в 3D мировые координаты.

    Аргументы:
        xy (tuple): Координаты пикселя (X, Y).
        dist (float): Расстояние от камеры до объекта.

    Возвращает:
        Point: 3D-координаты объекта.
    """
    xy_rect = camera_model.rectifyPoint(xy)
    ray = camera_model.projectPixelTo3dRay(xy_rect)
    return Point(x=ray[0] * dist, y=ray[1] * dist, z=dist)


def get_center_of_mass(mask: np.ndarray) -> tuple:
    """
    Вычисляет центр масс бинарной маски.

    Аргументы:
        mask (np.ndarray): Бинарная маска, где пиксели, принадлежащие объекту, равны 1.

    Возвращает:
        tuple: Координаты центра масс (X, Y).
    """
    M = cv2.moments(mask)
    if M['m00'] == 0:
        return None
    return M['m10'] // M['m00'], M['m01'] // M['m00']


def color_coord(mask: np.ndarray, color_name: str, msg) -> None:
    """
    Обрабатывает найденные цветные объекты, вычисляет их координаты и обновляет карту и лог.

    Аргументы:
        mask (np.ndarray): Бинарная маска цвета.
        color_name (str): Название цвета объекта.
        msg: Сообщение с заголовком и данными.
    """
    global count_models, buildings, img_map
    xy = get_center_of_mass(mask)
    if xy is None:
        return
    try:
        altitude = get_telemetry('terrain').z
    except rospy.ServiceException:
        return
    xy3d = img_xy_to_point(xy, altitude)
    target = PointStamped(header=msg.header, point=xy3d)
    try:
        setpoint = tf_buffer.transform(target, 'aruco_map', timeout=rospy.Duration(0.2))
    except rospy.ServiceException as e:
        rospy.logerr(f'Transform error: {e}')
        return

    if check_position(setpoint.point.x, setpoint.point.y, color_name):
        buildings += f'{model_name[color_name]}: X = {real_round(setpoint.point.x, 2)} Y = {real_round(setpoint.point.y, 2)} color = {color_name}\n'
        with open('static/otchet.txt', 'a') as file:
            file.write(f'{model_name[color_name]}:\n X = {real_round(setpoint.point.x, 2)} Y = {real_round(setpoint.point.y, 2)} color = {color_name}\n\n')

        rospy.loginfo(f'{color_text["red"]}[DEBUG]: {model_name[color_name]}: {color_text["yellow"]}{color_name} {color_text["white"]}X = {real_round(setpoint.point.x, 2)} Y = {real_round(setpoint.point.y, 2)}')

        opr_models[count_models] = [setpoint.point.x, setpoint.point.y, color_name]
        with open('static/coord_model.pkl', 'wb') as file_pkl:
            pickle.dump(opr_models, file_pkl)

        x, y, color = opr_models[count_models]
        bgr_color = colors[color]
        cv2.rectangle(img_map, 
                      (108 + int(real_round((x - 0.3) * length)), 892 - int(real_round((y - 0.3) * length))),
                      (112 + int(real_round((x + 0.3) * length)), 888 - int(real_round((y + 0.3) * length))),
                      (0, 0, 0), -1)
        cv2.rectangle(img_map,
                      (110 + int(real_round((x - 0.3) * length)), 890 - int(real_round((y - 0.3) * length))),
                      (110 + int(real_round((x + 0.3) * length)), 890 - int(real_round((y + 0.3) * length))),
                      bgr_color, -1)
        cv2.putText(img_map, f'color = {color_name}',
                    (90 + int(real_round((x - 0.3) * length)), 905 - int(real_round((y - 0.3) * length))),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 5)
        cv2.putText(img_map, f'color = {color_name}',
                    (90 + int(real_round((x - 0.3) * length)), 905 - int(real_round((y - 0.3) * length))),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, bgr_color, 2)
        cv2.putText(img_map, f'X = {real_round(setpoint.point.x, 2)}',
                    (100 + int(real_round((x - 0.3) * length)), 920 - int(real_round((y - 0.3) * length))),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 5)
        cv2.putText(img_map, f'X = {real_round(setpoint.point.x, 2)}',
                    (100 + int(real_round((x - 0.3) * length)), 920 - int(real_round((y - 0.3) * length))),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, bgr_color, 2)
        cv2.putText(img_map, f'Y = {real_round(setpoint.point.y, 2)}',
                    (100 + int(real_round((x - 0.3) * length)), 935 - int(real_round((y - 0.3) * length))),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 5)
        cv2.putText(img_map, f'Y = {real_round(setpoint.point.y, 2)}',
                    (100 + int(real_round((x - 0.3) * length)), 935 - int(real_round((y - 0.3) * length))),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, bgr_color, 2)

        cv2.imwrite('static/image.png', img_map)
        count_models += 1


shutdown_initiated = True


@long_callback
def image_callback(msg):
    """
    Обрабатывает входящее изображение от камеры, определяет объекты по цвету
    и обновляет карту с координатами.

    Аргументы:
        msg: Сообщение с изображением от камеры.
    """
    global buildings, shutdown_initiated
    if shutdown_initiated:
        return

    img = bridge.imgmsg_to_cv2(msg, 'bgr8')
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

    blue = cv2.inRange(hsv, (115, 150, 150), (125, 255, 255))
    green = cv2.inRange(hsv, (55, 150, 150), (65, 255, 255))
    red = cv2.inRange(hsv, (0, 150, 150), (5, 255, 255))
    yellow = cv2.inRange(hsv, (25, 150, 150), (35, 255, 255))

    if cv2.countNonZero(blue) > 10:
        color_coord(mask=blue, color_name='blue', msg=msg)
    elif cv2.countNonZero(green) > 10:
        color_coord(mask=green, color_name='green', msg=msg)
    elif cv2.countNonZero(red) > 10:
        color_coord(mask=red, color_name='red', msg=msg)
    elif cv2.countNonZero(yellow) > 10:
        color_coord(mask=yellow, color_name='yellow', msg=msg)

    if buildings:
        buildings_pub.publish(data=buildings)

    rospy.sleep(0.3)


def main():
    """
    Основная функция для навигации дрона и выполнения задач.
    """
    global count_models, shutdown_initiated, img_map, opr_models

    # Проверяем, если есть сохраненные данные о стадии или координатах моделей
    if os.path.exists('static/stage.pkl'):
        with open('static/stage.pkl', 'rb') as file:
            stage_nav = pickle.load(file)
        if os.path.exists('static/coord_model.pkl'):
            with open('static/coord_model.pkl', 'rb') as file_pkl:
                opr_models = pickle.load(file_pkl)
    else:
        stage_nav = 0
        img_map = cv2.resize(bridge.imgmsg_to_cv2(rospy.wait_for_message('aruco_map/image', Image), 'bgr8'), (1000, 1000))
        cv2.imwrite('static/image.png', img_map)
        try:
            os.remove('static/otchet.txt')
        except FileNotFoundError:
            pass
        with open('static/otchet.txt', 'a') as file:
            file.write('\n')

    # Навигация к стартовой точке
    navigate_wait(z=1.5, frame_id='body', auto_arm=True, speed=0.5)
    shutdown_initiated = False
    for stage, aruco_id in enumerate([9, 19, 10, 20, 29, 39, 30, 40, 49, 59, 50, 60, 69, 79, 70, 80, 89, 99, 90][stage_nav:], start=stage_nav):
        navigate_wait(z=1.5, frame_id=f'aruco_{aruco_id}')
        with open('static/stage.pkl', 'wb') as file:
            pickle.dump(stage + 1, file)
    
    # Возврат к начальной точке
    navigate_wait(z=1.5, frame_id='aruco_0', speed=0.7, tolerance=0.3, yaw=0)
    navigate_wait(z=0.2, frame_id='aruco_0', speed=0.3, tolerance=0.15, yaw=0)
    land_wait()

    # Очистка временных файлов
    os.remove('static/stage.pkl')
    os.remove('static/coord_model.pkl')
    shutdown_initiated = True
    rospy.sleep(0.3) 
    exit()


if __name__ == "__main__":
    # Запуск программы
    if '--land' not in sys.argv and '--kill' not in sys.argv:
        main()
        rospy.spin()
    elif '--land' in sys.argv and '--kill' not in sys.argv:
        land_wait()
        rospy.loginfo(f'{color_text["red"]}[LAND]{color_text["white"]}')
        exit()
    elif '--kill' in sys.argv and '--land' not in sys.argv:
        if get_telemetry().armed:
            set_attitude(thrust=0)
        else:
            rospy.loginfo(f'{color_text["red"]}Дрон не заармлен{color_text["white"]}')
        rospy.loginfo(f'{color_text["red"]}[KILL]{color_text["white"]}')
        exit()
Веб-приложение. Код решения:
HTML
<!DOCTYPE html>
<html>
  <head>
    <title>Control Panel</title>
    <style>
      body {
        background-color: #1c1c1c;
        color: #f0f0f0;
        font-family: Arial, sans-serif;
        margin: 0;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
      }

      /* Основной контейнер */
      .container {
        display: flex;
        flex-direction: row;
        align-items: center;
        gap: 20px;
        width: 95%;
        justify-content: space-between;
      }

      /* Кнопки*/
      .button-container {
        display: flex;
        flex-direction: column;
        gap: 70px;
        align-items: flex-start;
        margin-left: 100px;
      }

      /* Вид кнопок */
      .rounded-button {
        padding: 35px 70px;
        font-size: 32px;
        color: #fff;
        background-color: #333;
        border: none;
        border-radius: 20px;
        cursor: pointer;
        width: 350px;
        text-align: center;
        transition: all 0.3s ease;
      }

      /* Анимация наведения на кнопку*/
      .rounded-button:not(:disabled):hover {
        background-color: #555;
        transform: scale(1.1);
      }

      /* Вид неактивных кнопок */
      .rounded-button:disabled {
        background-color: #666;
        cursor: not-allowed;
        opacity: 0.5;
      }

      /* Изображения */
      .image-container img {
        max-width: 800px;
        border-radius: 20px;
        border: 5px solid #333;
        box-shadow: 0 8px 20px rgba(0, 0, 0, 0.5);
        margin-right: 100px;
      }

      /* Текстовое окно для buildings */
      .text-container {
        display: flex;
        flex-direction: column;
        gap: 20px;
        align-items: flex-start;
        margin-left: 10px;
        margin-right: 10px;
      }

      /* Вид этого же окна*/
      .text-box {
        padding: 10px;
        font-size: 20px;
        font-weight: bold;
        color: #fff;
        background-color: #333;
        border: none;
        border-radius: 20px;
        width: 350px;
        height: 440px;
        overflow-y: auto;
        box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
        white-space: pre-wrap;
        text-align: center;
      }
      /* Подпись */
      .signature {
        position: absolute;
        bottom: 10px;
        left: 10px;
        font-size: 15px;
        color: #888;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="button-container">
        <button class="rounded-button" id="start">START</button>
        <button class="rounded-button" id="stop" disabled>STOP</button>
        <button class="rounded-button" id="kill">KILL SWITCH</button>
      </div>
      <div class="text-container">
        <div class="text-box" id="buildings-text"></div>
      </div>
      <div class="image-container">
        <img src="image.png" alt="Control Image" />
      </div>
      <div class="signature">by Obradovich Vlad</div>
    </div>
    <script>
      document.addEventListener("DOMContentLoaded", () => {
        const startButton = document.getElementById("start");
        const stopButton = document.getElementById("stop");
        const killButton = document.getElementById("kill");
        const imageElement = document.querySelector(".image-container img");
        const buildingsTextElement = document.getElementById("buildings-text");

        const baseUrl = "/api";

        const updateButtons = (response) => {
          startButton.disabled = !response.start;
          stopButton.disabled = !response.stop;
          killButton.textContent = `KILL SWITCH`;
        };

        const sendRequest = async (button) => {
          try {
            const response = await fetch(`${baseUrl}/${button}`, {
              method: "POST",
              headers: {
                "Content-Type": "application/json",
              },
            });
            if (!response.ok) {
              throw new Error(`HTTP error! status: ${response.status}`);
            }
            const data = await response.json();
            updateButtons(data);
          } catch (error) {
            console.error("Error:", error);
          }
        };

        startButton.addEventListener("click", () => {
          if (!startButton.disabled) sendRequest("start");
        });

        stopButton.addEventListener("click", () => {
          if (!stopButton.disabled) sendRequest("stop");
        });

        killButton.addEventListener("click", () => sendRequest("kill"));

        // Получение начального состояния
        fetch(`${baseUrl}/state`)
          .then((response) => response.json())
          .then(updateButtons)
          .catch((error) => console.error("Error:", error));

        // Функция для обновления изображения
        const updateImage = () => {
          const timestamp = new Date().getTime();
          imageElement.src = `image.png?t=${timestamp}`;
        };

        //  Обновление изображения каждые 0.2 сек
        setInterval(updateImage, 200);

        // Функция для обновления текста
        const updateVariableText = async () => {
          try {
            const response = await fetch(`${baseUrl}/buildings_text`, {
              method: "GET",
              headers: {
                "Content-Type": "application/json",
              },
            });
            if (!response.ok) {
              throw new Error(`HTTP error! status: ${response.status}`);
            }
            const data = await response.json();
            buildingsTextElement.innerHTML = data.text.replace(/\n/g, "<br>");
          } catch (error) {
            console.error("Error:", error);
          }
        };

        // Обновление текста каждые 0.2 сек
        setInterval(updateVariableText, 200);
      });
    </script>
  </body>
</html>
text slider background image text slider background image
text slider background image text slider background image text slider background image text slider background image