This commit is contained in:
2026-09-09 16:28:02 +03:00
commit f48465dfb1
36 changed files with 3580 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
__pycache__/
*.py[cod]
.venv/
.idea/
.vscode/
+74
View File
@@ -0,0 +1,74 @@
# Актуальные заметки по соревнованию
## Рабочие допущения
- Будет отдельный день настройки оборудования.
- Основной ожидаемый формат — физические РМК, но Webots остаётся запасным и учебным вариантом.
- На физических РМК ожидается тот же ROS 2 API. Если площадка выдаёт другое окружение без документации и времени адаптации, это нужно сразу поднимать как проблему инфраструктуры.
- Интернет и документация разрешены.
- По телефонному разъяснению эксперта LLM разрешены, формат близок к хакатону. Это устное актуальное разъяснение; его стоит повторно зафиксировать на инструктаже.
- Целевой ArUco ID сообщает эксперт.
- Для модуля В, вероятно, понадобится новый датасет с выданной камеры и физических деталей либо готовый датасет организаторов.
- Для модуля Г важнее работоспособный интерфейс по критериям, чем сложный дизайн.
## Ограничение числа скриптов
По полному финальному КЗ:
| Модуль | Максимум взаимодействий/скриптов на зачётном ПК |
| --- | ---: |
| Б | 2 |
| В | 1 |
| Д | 4 |
Текущий `module5/module5.py`, запускаемый одним процессом, не нарушает ограничение модуля Д. Он не использует собственный UDP-протокол: один ROS 2 node на зачётном ПК публикует команды в пространства `/RMC1/*` и `/RMC2/*` и получает телеметрию обоих роботов через DDS.
Это работает, если оба физических РМК доступны в одном согласованном ROS-домене. Если площадка разведёт роботов по разным доменам или компьютерам, архитектуру нужно будет адаптировать в день настройки.
Официальный сценарий модуля Д допускает последовательные или параллельные операции. Текущее решение выполняет их последовательно — медленнее, но проще и безопаснее.
## Предлагаемые изменения 30%
### Модуль Б — навигация
- заменить ID стартового и целевого ArUco;
- переставить несколько закрытых ячеек;
- изменить место появления динамического препятствия;
- передавать старт, финиш и препятствия через конфиг.
Это меняет входные данные, но сохраняет планировщик, маршрут, возврат, объезд и оценку времени.
### Модуль В — техническое зрение
- выбирать другой целевой класс из заранее подготовленных классов;
- менять расположение детали перед камерой;
- менять цвет или номер целевой детали;
- не вводить неизвестную категорию и новую архитектуру непосредственно на площадке.
Если физические детали отличаются от симулятора, новый датасет всё равно потребуется. Классы и формат разметки лучше сохранить прежними.
### Модуль Г — GUI/FMS
- переключатель РМК-1/РМК-2;
- состояния `ONLINE/OFFLINE`, `AUTO/MANUAL/STOP`;
- перестановка блоков интерфейса;
- цвет свободных и занятых ячеек;
- окно последних событий;
- неизменные ROS-топики и сервисы.
### Модуль Д — складской сценарий
- менять ячейки хранения, комплектации и выдачи;
- выбирать другую известную деталь;
- менять порядок существующих операций;
- сохранять РМК, ARM95 и ROS-интерфейсы;
- хранить точки и класс детали в YAML/JSON или аргументах запуска.
### Модуль А — диагностика
- заменить диагностируемую плату;
- изменить набор заранее внесённых неисправностей;
- использовать единый шаблон отчёта;
- ограничить дефекты теми, которые обнаруживаются осмотром и мультиметром без пайки.
Этот вариант безопасен только при наличии схемы или документации на плату и заранее определённого перечня допустимых неисправностей.
+248
View File
@@ -0,0 +1,248 @@
# ЧВТ-2026: документы и памятка по подготовке
Этот приватный репозиторий — офлайн-набор для подготовки: исходные материалы организаторов, порядок установки окружения, ссылки на документацию и короткие ROS-примеры. Рабочие решения модулей лежат отдельно в [`mavxa/mvch`](https://github.com/mavxa/mvch).
> Конкурсное задание, критерии и указания экспертов на площадке имеют приоритет над этой памяткой и старыми презентациями. Не меняйте мир Webots, расположение объектов и штатные управляющие пакеты, если это прямо не разрешено экспертами.
## Что лежит в репозитории
- [`materials/presentations`](materials/presentations) — пять учебных презентаций (введение, документация, Python/ROS, симулятор, развитие компетенции).
- [`materials/pdf`](materials/pdf) — шестая презентация про датасет и модели, расписание.
- [`materials/final`](materials/final) — полный очный финал: инфраструктурный лист, КЗ модулей А–Д, критерии и приложения.
- [`materials/tasks`](materials/tasks) — отдельный ранний/отборочный комплект по навигации; не путать с полным финалом.
- [`basic`](basic) — примеры для реального ROS 2: движение к ArUco с odometry/TF/lidar, ручной `cmd_vel`, остановка, лифт, мониторинг и BFS-маршрут.
- [`setup/FROM_ZERO.md`](setup/FROM_ZERO.md) — подготовка полностью чистого Windows/Ubuntu и день настройки оборудования.
- [`setup/COMMANDS.md`](setup/COMMANDS.md) — только команды для копирования в терминал.
- [`setup/check_environment.sh`](setup/check_environment.sh) — безопасная read-only проверка окружения.
- [`COMPETITION_NOTES.md`](COMPETITION_NOTES.md) — актуальные устные уточнения, лимиты скриптов и предложения 30% изменений.
Материалы собраны из выданного набора ЧВТ. Репозиторий оставлен приватным: права на исходные презентации и задание принадлежат их авторам/организаторам.
Актуальную опубликованную редакцию сверяйте с [официальным комплектом финала 2026](https://esim.firpo.ru/d/f/2026), раздел «Оператор-техник складских роботов (AGV/AMR)».
## Порядок чтения
1. `materials/final/04-final-task.docx` и `materials/final/05-final-criteria.xlsx` — сначала понять конечный результат и баллы очного финала.
2. `1. Введение...` и `2. Документация...` — термины, РМК и структура компетенции.
3. `3. Разбор...` — Python, ROS 2, топики и сервисы.
4. `4. Практика... (симулятор)` — установка и работа с Webots.
5. `5. Практика... (датасет + модели)` — разметка и обучение детектора.
6. `setup/FROM_ZERO.md` — подготовка ПК и оборудования.
7. README каждого модуля в `mvch` — точные команды текущего решения.
## Быстрый старт на подготовленной VM
Нужны SSH-ключ с доступом к приватным GitHub-репозиториям и уже установленное окружение ROS/Webots.
```bash
mkdir -p ~/scripts
git clone git@github.com:mavxa/mvch-docs.git ~/scripts/mvch-docs
git clone git@github.com:mavxa/mvch.git ~/scripts/mvch
cd ~/scripts/mvch-docs
./setup/check_environment.sh
```
В каждом новом ROS-терминале:
```bash
source /opt/ros/jazzy/setup.bash
source ~/ros2_ws/install/setup.bash
```
Перед поездкой обновлять только fast-forward:
```bash
cd ~/scripts/mvch
git switch main
git pull --ff-only
```
## Установка чистого окружения
Официальная целевая база материалов — **Ubuntu 24.04 LTS + ROS 2 Jazzy + Webots + Fast DDS + colcon**. На площадке лучше использовать выданный и заранее проверенный образ, если он будет предоставлен.
Полный путь от чистой Windows/Ubuntu, включая VS Code, расширения, Git/SSH, ROS, Webots, Python/YOLO, Bun, Node.js и OpenCode: [`setup/FROM_ZERO.md`](setup/FROM_ZERO.md). Краткий файл только с командами: [`setup/COMMANDS.md`](setup/COMMANDS.md).
### 1. ROS 2 Jazzy
Устанавливайте Jazzy по [официальной инструкции ROS 2 для Ubuntu](https://docs.ros.org/en/jazzy/Installation/Ubuntu-Install-Debs.html), включая настройку locale и репозитория ROS. Минимальный набор после подключения репозитория:
```bash
sudo apt update
sudo apt install ros-jazzy-desktop ros-dev-tools python3-colcon-common-extensions \
python3-rosdep python3-venv git
source /opt/ros/jazzy/setup.bash
```
`rosdep init` выполняется один раз на систему:
```bash
sudo rosdep init
rosdep update
```
Если команда сообщает, что sources list уже существует, повторный `rosdep init` не нужен.
### 2. Webots
Сверяйте установку с [официальной инструкцией Cyberbotics](https://cyberbotics.com/doc/guide/installing-webots). Версия симулятора должна совпадать с версией, под которую собран выданный ROS-пакет; самовольно обновлять её прямо перед соревнованием не стоит.
### 3. Пакеты симулятора ЧВТ
Команды из выданных материалов:
```bash
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
git clone https://gitlab.mobird.dev/fms_group/ar_webots_fms_ros2.git
git clone https://gitlab.mobird.dev/fms_group/ar_arm95_moveit_config.git
git clone https://gitlab.mobird.dev/fms_group/ar_aruco_detect_ros2.git
git clone https://gitlab.mobird.dev/fms_group/ar_dual_lidar_merge_ros2.git
git clone https://gitlab.mobird.dev/fms_group/ar_nav_ros2.git
cd ~/ros2_ws
rosdep install --from-paths src -y --ignore-src
colcon build --symlink-install
source ~/ros2_ws/install/setup.bash
```
GitLab может потребовать выданный организаторами аккаунт/токен. Если clone не работает, не ищите случайные зеркала: запросите доступ у эксперта.
### 4. Python-окружение YOLO и ROS
ROS-библиотеки установлены системно, поэтому venv для модуля В создаётся с `--system-site-packages`. Это также предотвращает несовместимость отдельного Python с бинарными пакетами ROS; подробнее — в [официальной памятке ROS 2 по Python-пакетам](https://docs.ros.org/en/jazzy/How-To-Guides/Using-Python-Packages.html).
```bash
cd ~/scripts/mvch/module3
python3 -m venv --system-site-packages .venv
.venv/bin/python -m pip install -U pip
.venv/bin/pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
.venv/bin/pip install -r requirements.txt tqdm ultralytics-thop
```
В текущем образе `cv_bridge` требует NumPy 1.x:
```bash
.venv/bin/pip install --force-reinstall "numpy<2" "opencv-python<4.12" lark transforms3d
```
Для NVIDIA/ROCm сначала установите подходящую сборку PyTorch по [официальному селектору PyTorch](https://pytorch.org/get-started/locally/), затем остальные зависимости. Для CPU готовая команда выше достаточна.
### 5. Веб-интерфейс модуля Г
Установите [Bun](https://bun.sh/docs/installation), затем:
```bash
cd ~/scripts/mvch/module4
bun install
bun run build
```
## Быстрая проверка перед работой
```bash
cd ~/scripts/mvch-docs
./setup/check_environment.sh
source /opt/ros/jazzy/setup.bash
source ~/ros2_ws/install/setup.bash
ros2 topic list
ros2 pkg prefix ar_webots_fms_ros2
```
Проверьте только один launch-файл за раз. После его запуска отдельно убедитесь, что приходят сообщения:
```bash
ros2 topic hz /RMC1/odometry
ros2 topic echo /RMC1/odometry --once
ros2 topic echo /RMC2/scan_front --once
```
Точный список топиков зависит от активного launch-файла. Если `scan_front` отсутствует, сначала посмотрите `ros2 topic list | sort`, а не меняйте код вслепую.
## Запуск модулей
| Модуль | Решение | Основной запуск |
| --- | --- | --- |
| Б | навигация RMC2 по ArUco | `python3 module2/module_b.py --target 14 --sim` |
| В | YOLO + ARM95 | `module3/.venv/bin/python module3/module3.py --target hammer` |
| Г | веб-FMS двух роверов | `cd module4 && bun run start` |
| Д | общий сценарий RMC1 + RMC2 | `module3/.venv/bin/python module5/module5.py --target hammer` |
Запускайте команды из `~/scripts/mvch`. До движения используйте dry-run:
```bash
python3 module2/module_b.py --target 14 --dry-run
module3/.venv/bin/python module3/module3.py --target hammer --dry-run --no-window
python3 module5/module5.py --target hammer --dry-run
```
Для модуля Д сначала проверяйте платформы и лифт без руки: `python3 module5/module5.py --target hammer --skip-arm`.
Лимит полного финального КЗ: модуль Б — максимум 2 скрипта, В — 1, Д — 4. Текущий модуль Д использует один центральный ROS 2 процесс для обоих пространств имён и укладывается в лимит; отдельного UDP-протокола нет.
## Модель для симулятора
Текущая YOLO11n лежит в `mvch/module3/models/latest.pt`. Классы и их написание:
```text
hammer
pliers
wrench
```
Текущий симуляторный датасет очень маленький: 28 train, 8 validation и 4 test изображения. Он годится для проверки пайплайна, но не доказывает работу на реальном стенде. Для реального оборудования нужен отдельный набор кадров с его камеры, светом, фоном, ракурсами и реальными карточками/деталями. Инструкции обучения — в `mvch/module3/README.md` и [Ultralytics Train mode](https://docs.ultralytics.com/modes/train/).
## План подготовки
1. Сохранить рабочую VM и проверить clone обоих репозиториев без старых локальных файлов.
2. Для каждого launch-файла записать реальные `ros2 topic list`, стартовые ID и положения объектов.
3. Пройти модуль Б на нескольких целях и с препятствием у стеллажа; проверить остановку при потере ArUco/лидара.
4. В модуле В сначала подтвердить dry-run координат, затем pick/place на каждом классе.
5. В модуле Г проверить ручной STOP, телеметрию обоих роверов и fallback без Nav2.
6. В модуле Д пройти `--skip-arm`, затем полный последовательный сценарий.
7. Сделать чистый контрольный прогон строго по процедуре эксперта и сохранить логи.
8. На площадке сначала снять фактические топики, TF, размеры, тормозной путь и геометрию ARM95; только потом менять параметры.
## Что уточнить у главного эксперта
- Финал проходит полностью в Webots, на физических РМК или в смешанном формате?
- Какая редакция конкурсного задания и критериев считается действующей?
- Будет ли готовая VM, доступ к закрытому GitLab и разрешено ли принести свой Git-репозиторий?
- Разрешены ли интернет, локальная документация, поисковики и LLM; как технически фиксируются нарушения?
- Какие действия участника допустимы между этапами: Enter, stop/resume, выбор цели, ручное подтверждение маршрута?
- Какие точки/ID сообщаются перед попыткой, а какие должны определяться автоматически?
- Что именно входит в разрешённые 30% изменений задания?
Ответы лучше получить письменно или зафиксировать в протоколе инструктажа.
## Документация и форумы
- [ROS 2 Jazzy documentation](https://docs.ros.org/en/jazzy/)
- [ROS 2 concepts](https://docs.ros.org/en/jazzy/Concepts.html)
- [ROS 2 tf2 tutorials](https://docs.ros.org/en/jazzy/Tutorials/Intermediate/Tf2/Tf2-Main.html)
- [webots_ros2_driver API](https://docs.ros.org/en/jazzy/p/webots_ros2_driver/)
- [Webots User Guide](https://cyberbotics.com/doc/guide/index)
- [Webots Discussions](https://github.com/cyberbotics/webots/discussions)
- [MoveIt 2 documentation](https://moveit.picknik.ai/main/index.html)
- [MoveIt 2 issues](https://github.com/moveit/moveit2/issues)
- [Ultralytics documentation](https://docs.ultralytics.com/)
- [Roboflow Annotate documentation](https://docs.roboflow.com/annotate)
- [Robotics Stack Exchange: ROS 2](https://robotics.stackexchange.com/questions/tagged/ros2) — конкретные технические вопросы с логами и минимальным примером.
- [Open Robotics Discourse](https://discourse.ros.org/) — новости и общие обсуждения экосистемы; не основная площадка техподдержки.
Публичного официального форума именно по этому заданию ЧВТ в выданных материалах не указано. Вопросы о правилах, критериях, доступе к репозиториям и оборудованию адресуйте главному эксперту, а не стороннему форуму.
## Git на соревновании
Для критериев обычно достаточно локальной истории: `main`, понятные коммиты и при необходимости ветки модулей. GitHub удобен как резервная копия, но не рассчитывайте на интернет на площадке.
```bash
git status
git log --oneline --decorate --graph --all -20
git switch main
```
Не выполняйте `git reset --hard`, `git clean` и force-push перед попыткой. Скопируйте оба репозитория и веса модели на внешний накопитель или в снапшот VM.
+74
View File
@@ -0,0 +1,74 @@
# Базовые примеры для реального ROS 2
Это короткие учебные скрипты, которые работают через штатные ROS 2 интерфейсы
роверов: `cmd_vel`, `odometry`, `scan_front`, `aruco_id`, TF и `lift`. В них нет
прямого управления моторами и нет подмены симулятора. Один и тот же ROS API можно
проверять в Webots, а затем перенести на физический РМК после сверки имён топиков,
TF, размеров поля и безопасных скоростей.
Это не готовые конкурсные решения и не замена аппаратному аварийному STOP.
Перед запуском:
```bash
source /opt/ros/jazzy/setup.bash
source ~/ros2_ws/install/setup.bash
export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
```
Сначала проверьте реальные интерфейсы:
```bash
ros2 topic list | sort
ros2 topic info /RMC2/cmd_vel -v
ros2 topic info /RMC2/odometry -v
ros2 topic info /RMC2/scan_front -v
ros2 topic echo /RMC2/aruco_id
ros2 run tf2_ros tf2_echo RMC2/odom aruco_0
```
Практические примеры:
```bash
# Проехать RMC2 от текущей метки до ArUco 14 на физическом ROS
python3 basic/drive_to_aruco.py 14
# То же в Webots; только здесь нужен --sim
python3 basic/drive_to_aruco.py 14 --sim
# Закрытые клетки и уменьшенная реальная скорость
python3 basic/drive_to_aruco.py 14 --blocked 8 9 --max-speed 0.08
# Короткая проверка движения; lidar обязателен, после 1 с будет stop
python3 basic/move_rmc.py --robot RMC1 --x 0.15 --seconds 1
# Поворот RMC2 на месте
python3 basic/move_rmc.py --robot RMC2 --yaw 0.3 --seconds 1
# Статус положения и лидара
python3 basic/watch_robot.py --robot RMC1
# Управление лифтом RMC2
python3 basic/lift_rmc2.py up
python3 basic/lift_rmc2.py down
# Остановить оба ровера
python3 basic/stop_all.py
```
`drive_to_aruco.py` рассчитан на регулярную сетку 6×6: `0=(0,0)`,
`1=(0,+1 м)`, `6=(-1 м,0)`. Ровер должен стартовать центром над видимой меткой.
Скрипт сам определяет старт, строит BFS-маршрут, печатает его до движения, затем
едет по odometry и уточняет положение через TF каждой ArUco. Без свежих odometry,
лидара, метки на финише или при препятствии ближе `--stop-distance` он отправляет
нулевой `Twist` и завершает работу.
На реальном поле сначала измерьте шаг сетки и при необходимости задайте
`--spacing`. Если организаторы используют другой топик лидара, передайте
`--scan-topic /нужный/топик`. Не используйте `--yes` в первом физическом заезде.
У RMC1 доступно боковое движение (`linear.y`), у RMC2 оно принудительно запрещено
в `move_rmc.py`. Режим `--no-lidar` предназначен только для вывешенного стенда,
где колёса не могут переместить робот.
`grid_route.py` оставлен как офлайн-проверка графа. Он ничего не публикует в ROS.
+346
View File
@@ -0,0 +1,346 @@
#!/usr/bin/env python3
"""Безопасно проехать на RMC по сетке до указанной ArUco-метки.
Скрипт использует штатные ROS 2 топики и TF. По умолчанию рассчитан на
физический робот; для Webots явно передайте --sim.
"""
import argparse
import math
import re
import time
from collections import deque
import rclpy
from geometry_msgs.msg import Twist
from nav_msgs.msg import Odometry
from rclpy.node import Node
from rclpy.parameter import Parameter
from rclpy.qos import qos_profile_sensor_data
from rclpy.time import Time
from sensor_msgs.msg import LaserScan
from std_msgs.msg import String
from tf2_ros import Buffer, TransformException, TransformListener
def clamp(value, low, high):
return max(low, min(high, value))
def wrap(angle):
return math.atan2(math.sin(angle), math.cos(angle))
def yaw(quaternion):
return math.atan2(
2 * (quaternion.w * quaternion.z + quaternion.x * quaternion.y),
1 - 2 * (quaternion.y**2 + quaternion.z**2),
)
def compose(a, b):
"""Перенести позу b из системы a в родительскую систему a."""
cosine, sine = math.cos(a[2]), math.sin(a[2])
return (
a[0] + cosine * b[0] - sine * b[1],
a[1] + sine * b[0] + cosine * b[1],
wrap(a[2] + b[2]),
)
def inverse(pose):
cosine, sine = math.cos(pose[2]), math.sin(pose[2])
return (
-cosine * pose[0] - sine * pose[1],
sine * pose[0] - cosine * pose[1],
-pose[2],
)
def marker_pose(marker, columns, spacing):
"""Штатная схема ЧВТ: 0=(0,0), 1=(0,+S), COLS=(-S,0)."""
row, column = divmod(marker, columns)
return -row * spacing, column * spacing, 0.0
def build_route(start, target, rows, columns, blocked):
queue = deque([start])
previous = {start: None}
while queue:
current = queue.popleft()
if current == target:
break
row, column = divmod(current, columns)
for next_row, next_column in (
(row - 1, column),
(row + 1, column),
(row, column - 1),
(row, column + 1),
):
candidate = next_row * columns + next_column
if not (0 <= next_row < rows and 0 <= next_column < columns):
continue
if candidate in blocked or candidate in previous:
continue
previous[candidate] = current
queue.append(candidate)
if target not in previous:
raise RuntimeError(f"Нет маршрута {start} -> {target}")
route = []
current = target
while current is not None:
route.append(current)
current = previous[current]
return list(reversed(route))
class ArucoDrive(Node):
def __init__(self, args):
super().__init__("mvch_basic_aruco_drive")
self.args = args
self.robot = args.robot.strip("/")
if args.sim:
self.set_parameters([Parameter("use_sim_time", value=True)])
self.odom = None
self.pose = None
self.map_from_odom = None
self.marker = None
self.marker_error = math.inf
self.front = None
self.odom_wall = 0.0
self.marker_wall = 0.0
self.scan_wall = 0.0
self.odom_frame = f"{self.robot}/odom"
self.tf_buffer = Buffer()
self.tf_listener = TransformListener(self.tf_buffer, self)
self.publisher = self.create_publisher(
Twist, f"/{self.robot}/cmd_vel", 10
)
self.create_subscription(
Odometry,
f"/{self.robot}/odometry",
self.on_odometry,
qos_profile_sensor_data,
)
self.create_subscription(
LaserScan,
args.scan_topic or f"/{self.robot}/scan_front",
self.on_scan,
qos_profile_sensor_data,
)
self.create_subscription(
String, f"/{self.robot}/aruco_id", self.on_marker, 10
)
def on_odometry(self, message):
pose = message.pose.pose
values = (pose.position.x, pose.position.y, yaw(pose.orientation))
if all(math.isfinite(value) for value in values):
self.odom = values
self.odom_wall = time.monotonic()
def on_marker(self, message):
match = re.fullmatch(r"(?:aruco_)?(\d+)", message.data.strip())
if match:
marker = int(match.group(1))
if 0 <= marker < self.args.rows * self.args.columns:
self.marker = marker
self.marker_wall = time.monotonic()
def on_scan(self, message):
if not message.ranges or message.angle_increment == 0:
return
distances = []
for index, distance in enumerate(message.ranges):
angle = message.angle_min + index * message.angle_increment
if abs(angle) > math.radians(25):
continue
if math.isfinite(distance) and message.range_min <= distance <= message.range_max:
distances.append(distance)
# Пустое пространство lidar часто кодирует как inf. Это свежий скан,
# просто без препятствий в выбранном секторе.
self.front = min(distances, default=math.inf)
self.scan_wall = time.monotonic()
def localize(self):
"""Привязать odometry к сетке по последней видимой метке."""
if self.odom is None or self.marker is None:
return
try:
transform = self.tf_buffer.lookup_transform(
self.odom_frame, f"aruco_{self.marker}", Time()
).transform
except TransformException:
return
odom_marker = (
transform.translation.x,
transform.translation.y,
yaw(transform.rotation),
)
correction = compose(
marker_pose(self.marker, self.args.columns, self.args.spacing),
inverse(odom_marker),
)
new_pose = compose(correction, self.odom)
if self.pose is not None:
jump = math.hypot(new_pose[0] - self.pose[0], new_pose[1] - self.pose[1])
if jump > 0.35:
return
self.map_from_odom = correction
self.pose = new_pose
self.marker_error = math.hypot(
odom_marker[0] - self.odom[0], odom_marker[1] - self.odom[1]
)
def update(self):
rclpy.spin_once(self, timeout_sec=0.05)
self.localize()
if self.map_from_odom is not None and self.odom is not None:
self.pose = compose(self.map_from_odom, self.odom)
def stop(self):
self.publisher.publish(Twist())
def wait_until_ready(self):
print("Жду odometry, scan_front, aruco_id и TF метки...")
deadline = time.monotonic() + self.args.ready_timeout
while rclpy.ok() and time.monotonic() < deadline:
self.update()
self.stop()
now = time.monotonic()
if (
self.pose is not None
and self.front is not None
and now - self.odom_wall < 1.0
and now - self.scan_wall < 1.0
and now - self.marker_wall < 1.0
):
return
raise RuntimeError(
"Датчики не готовы. Проверьте ros2 topic list, aruco_id и TF aruco_<ID>."
)
def check_safety(self):
now = time.monotonic()
if now - self.odom_wall > 1.0:
raise RuntimeError("Потеря odometry")
if now - self.scan_wall > 1.0:
raise RuntimeError("Потеря scan_front")
if self.front < self.args.stop_distance:
raise RuntimeError(
f"Препятствие спереди: {self.front:.2f} м "
f"(< {self.args.stop_distance:.2f} м)"
)
def drive_to(self, target):
target_pose = marker_pose(target, self.args.columns, self.args.spacing)
deadline = time.monotonic() + self.args.waypoint_timeout
while rclpy.ok() and time.monotonic() < deadline:
self.update()
self.check_safety()
dx = target_pose[0] - self.pose[0]
dy = target_pose[1] - self.pose[1]
distance = math.hypot(dx, dy)
marker_is_fresh = (
self.marker == target
and time.monotonic() - self.marker_wall < 1.0
and self.marker_error <= self.args.tolerance
)
if distance <= self.args.tolerance and marker_is_fresh:
self.stop()
print(f"Метка {target} достигнута, ошибка {self.marker_error:.3f} м")
return
angle_error = wrap(math.atan2(dy, dx) - self.pose[2])
command = Twist()
if abs(angle_error) > 0.15:
command.angular.z = clamp(
1.4 * angle_error,
-self.args.max_angular,
self.args.max_angular,
)
else:
command.linear.x = min(self.args.max_speed, 0.5 * distance)
command.angular.z = clamp(
1.2 * angle_error,
-self.args.max_angular,
self.args.max_angular,
)
self.publisher.publish(command)
raise RuntimeError(f"Таймаут движения к метке {target}")
def arguments():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("target", type=int, help="ID целевой ArUco")
parser.add_argument("--robot", default="RMC2", help="RMC1 или RMC2")
parser.add_argument("--rows", type=int, default=6)
parser.add_argument("--columns", type=int, default=6)
parser.add_argument("--spacing", type=float, default=1.0, help="Шаг сетки, м")
parser.add_argument("--blocked", type=int, nargs="*", default=[])
parser.add_argument("--max-speed", type=float, default=0.12, help="м/с")
parser.add_argument("--max-angular", type=float, default=0.30, help="рад/с")
parser.add_argument("--stop-distance", type=float, default=0.55, help="м")
parser.add_argument("--tolerance", type=float, default=0.06, help="м")
parser.add_argument("--ready-timeout", type=float, default=15.0, help="с")
parser.add_argument("--waypoint-timeout", type=float, default=45.0, help="с")
parser.add_argument("--scan-topic", help="Например /RMC2/scan_front")
parser.add_argument("--yes", action="store_true", help="Не ждать Enter")
parser.add_argument("--sim", action="store_true", help="Использовать /clock Webots")
args = parser.parse_args()
count = args.rows * args.columns
if args.rows <= 0 or args.columns <= 0 or not 0 <= args.target < count:
parser.error(f"target должен быть в диапазоне 0..{count - 1}")
if args.spacing <= 0 or args.max_speed <= 0 or args.max_angular <= 0:
parser.error("spacing и скорости должны быть больше нуля")
return args
def main():
args = arguments()
rclpy.init()
node = ArucoDrive(args)
exit_code = 0
try:
node.wait_until_ready()
start = node.marker
blocked = set(args.blocked) - {start, args.target}
route = build_route(
start, args.target, args.rows, args.columns, blocked
)
print(f"Стартовая метка: {start}")
print("Маршрут:", " -> ".join(map(str, route)))
print(
f"Топики: /{node.robot}/cmd_vel, /{node.robot}/odometry, "
f"/{node.robot}/aruco_id, {args.scan_topic or f'/{node.robot}/scan_front'}"
)
if not args.yes:
input("Проверьте поле и держите аппаратный STOP. Enter — начать: ")
# Во время input ROS не обрабатывал сообщения: перед движением снова
# получить свежие odometry/lidar/ArUco.
node.wait_until_ready()
for marker in route[1:]:
node.drive_to(marker)
node.stop()
print(f"FINISHED: ровер стоит над ArUco {args.target}")
except (KeyboardInterrupt, RuntimeError) as error:
exit_code = 1
print(f"STOP: {error}")
finally:
for _ in range(10):
node.stop()
rclpy.spin_once(node, timeout_sec=0.02)
node.destroy_node()
rclpy.shutdown()
raise SystemExit(exit_code)
if __name__ == "__main__":
main()
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Строит кратчайший маршрут по сетке маркеров 6x6 без ROS."""
import argparse
from collections import deque
SIZE = 6
def neighbors(marker: int):
row, column = divmod(marker, SIZE)
for dr, dc in ((-1, 0), (1, 0), (0, -1), (0, 1)):
next_row, next_column = row + dr, column + dc
if 0 <= next_row < SIZE and 0 <= next_column < SIZE:
yield next_row * SIZE + next_column
def build_route(start: int, target: int, blocked: set[int]):
queue = deque([start])
previous = {start: None}
while queue:
current = queue.popleft()
if current == target:
break
for candidate in neighbors(current):
if candidate not in blocked and candidate not in previous:
previous[candidate] = current
queue.append(candidate)
if target not in previous:
return None
route = []
current = target
while current is not None:
route.append(current)
current = previous[current]
return list(reversed(route))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("start", type=int, help="Стартовый маркер 0..35")
parser.add_argument("target", type=int, help="Целевой маркер 0..35")
parser.add_argument(
"--blocked",
type=int,
nargs="*",
default=[],
help="Маркеры, через которые нельзя ехать",
)
args = parser.parse_args()
if not 0 <= args.start < SIZE * SIZE or not 0 <= args.target < SIZE * SIZE:
parser.error("start и target должны быть от 0 до 35")
blocked = set(args.blocked)
blocked.discard(args.start)
blocked.discard(args.target)
route = build_route(args.start, args.target, blocked)
if route is None:
raise SystemExit("Маршрут не найден")
print(" -> ".join(map(str, route)))
if __name__ == "__main__":
main()
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Поднимает или опускает лифт RMC2 через штатный ROS-топик."""
import argparse
import time
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float64, String
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("state", choices=("up", "down"))
parser.add_argument("--wait", type=float, default=6.0, help="Сколько ждать статус, с")
args = parser.parse_args()
rclpy.init()
node = Node("mvch_basic_lift")
publisher = node.create_publisher(Float64, "/RMC2/lift", 10)
statuses = []
node.create_subscription(String, "/RMC2/lift_status", lambda msg: statuses.append(msg.data), 10)
height = 0.1 if args.state == "up" else 0.0
print(f"RMC2 lift: {args.state}, команда {height}")
for _ in range(5):
publisher.publish(Float64(data=height))
rclpy.spin_once(node, timeout_sec=0.1)
deadline = time.monotonic() + args.wait
while time.monotonic() < deadline and not statuses:
rclpy.spin_once(node, timeout_sec=0.1)
if statuses:
print(f"Статус: {statuses[-1]}")
else:
print("Нового lift_status нет. Это возможно, если лифт уже был в нужном положении.")
node.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""Подаёт ограниченную по времени ROS-команду скорости и затем останавливает RMC."""
import argparse
import math
import time
import rclpy
from geometry_msgs.msg import Twist
from rclpy.node import Node
from rclpy.qos import qos_profile_sensor_data
from sensor_msgs.msg import LaserScan
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--robot", choices=("RMC1", "RMC2"), required=True)
parser.add_argument("--x", type=float, default=0.0, help="Скорость вперёд, м/с")
parser.add_argument("--y", type=float, default=0.0, help="Боковая скорость RMC1, м/с")
parser.add_argument("--yaw", type=float, default=0.0, help="Угловая скорость, рад/с")
parser.add_argument("--seconds", type=float, default=1.0)
parser.add_argument("--stop-distance", type=float, default=0.55, help="Стоп по lidar, м")
parser.add_argument("--no-lidar", action="store_true", help="Только стенд без препятствий")
args = parser.parse_args()
if args.seconds <= 0:
parser.error("--seconds должен быть больше нуля")
if args.robot == "RMC2" and args.y != 0:
parser.error("RMC2 не поддерживает боковое движение: используйте --y 0")
rclpy.init()
node = Node("mvch_basic_move")
publisher = node.create_publisher(Twist, f"/{args.robot}/cmd_vel", 10)
front_distance = [None]
def on_scan(message):
if not message.ranges or message.angle_increment == 0:
return
front = []
for index, distance in enumerate(message.ranges):
angle = message.angle_min + index * message.angle_increment
if abs(angle) <= 0.44 and math.isfinite(distance):
if message.range_min <= distance <= message.range_max:
front.append(distance)
front_distance[0] = min(front, default=math.inf)
if not args.no_lidar:
node.create_subscription(
LaserScan,
f"/{args.robot}/scan_front",
on_scan,
qos_profile_sensor_data,
)
command = Twist()
command.linear.x = args.x
command.linear.y = args.y
command.angular.z = args.yaw
print(f"{args.robot}: cmd_vel x={args.x}, y={args.y}, yaw={args.yaw}")
if not args.no_lidar:
print(f"Жду /{args.robot}/scan_front...")
deadline = time.monotonic() + 3.0
while front_distance[0] is None and time.monotonic() < deadline:
rclpy.spin_once(node, timeout_sec=0.1)
if front_distance[0] is None:
node.destroy_node()
rclpy.shutdown()
raise SystemExit("Нет scan_front: движение запрещено. Для стенда: --no-lidar")
started = time.monotonic()
exit_code = 0
try:
while time.monotonic() - started < args.seconds:
if front_distance[0] is not None and front_distance[0] < args.stop_distance:
raise RuntimeError(
f"Препятствие {front_distance[0]:.2f} м — аварийный стоп"
)
publisher.publish(command)
rclpy.spin_once(node, timeout_sec=0.05)
except RuntimeError as error:
exit_code = 1
print(f"STOP: {error}")
finally:
stop = Twist()
for _ in range(5):
publisher.publish(stop)
rclpy.spin_once(node, timeout_sec=0.05)
node.destroy_node()
rclpy.shutdown()
raise SystemExit(exit_code)
if __name__ == "__main__":
main()
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
"""Публикует нулевую скорость обоим роверам."""
import time
import rclpy
from geometry_msgs.msg import Twist
from rclpy.node import Node
def main():
rclpy.init()
node = Node("mvch_basic_stop_all")
publishers = [
node.create_publisher(Twist, "/RMC1/cmd_vel", 10),
node.create_publisher(Twist, "/RMC2/cmd_vel", 10),
]
stop = Twist()
for _ in range(10):
for publisher in publishers:
publisher.publish(stop)
rclpy.spin_once(node, timeout_sec=0.05)
time.sleep(0.02)
print("Нулевая скорость отправлена RMC1 и RMC2")
node.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Показывает одометрию и минимальную дистанцию перед ровером."""
import argparse
import math
import time
import rclpy
from nav_msgs.msg import Odometry
from rclpy.node import Node
from rclpy.qos import qos_profile_sensor_data
from sensor_msgs.msg import LaserScan
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--robot", choices=("RMC1", "RMC2"), required=True)
args = parser.parse_args()
rclpy.init()
node = Node("mvch_basic_watch")
state = {"pose": None, "front": None}
def on_odom(message):
position = message.pose.pose.position
orientation = message.pose.pose.orientation
siny = 2.0 * (orientation.w * orientation.z + orientation.x * orientation.y)
cosy = 1.0 - 2.0 * (orientation.y**2 + orientation.z**2)
state["pose"] = (position.x, position.y, math.atan2(siny, cosy))
def on_scan(message):
front = []
for index, distance in enumerate(message.ranges):
angle = message.angle_min + index * message.angle_increment
if abs(angle) <= math.radians(20) and math.isfinite(distance):
if message.range_min <= distance <= message.range_max:
front.append(distance)
state["front"] = min(front, default=math.inf)
node.create_subscription(
Odometry, f"/{args.robot}/odometry", on_odom, qos_profile_sensor_data
)
node.create_subscription(
LaserScan, f"/{args.robot}/scan_front", on_scan, qos_profile_sensor_data
)
print(f"Слушаю {args.robot}. Ctrl+C — выход.")
last_print = 0.0
try:
while rclpy.ok():
rclpy.spin_once(node, timeout_sec=0.1)
if time.monotonic() - last_print >= 0.5:
pose = state["pose"]
front = state["front"]
pose_text = "нет odometry" if pose is None else (
f"x={pose[0]:.2f}, y={pose[1]:.2f}, yaw={math.degrees(pose[2]):.1f}°"
)
front_text = "нет scan_front" if front is None else f"{front:.2f} м"
print(f"{pose_text}; спереди: {front_text}")
last_print = time.monotonic()
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+132
View File
@@ -0,0 +1,132 @@
ПРОГРАММА ПРОВЕДЕНИЯ
соревнований по компетенции
Оператор-техник складских роботов (AGV/AMR)
Финала Чемпионата высоких технологий в 2026 г
Великий Новгород
Общая информация
Период проведения 13-18 сентября
Место проведения и адрес площадки Великий Новгород
ФИО Главного эксперта Даукаев Руслан Ильнурович
Контакты Главного эксперта 79856178554
Д-2 / «13» сентября 2026 г.
09:00 – 09:30 Регистрация экспертов на площадке: сверка паспортов, подписание протокола.
09:30 – 10:00 Проведение инструктажа по охране труда, подписание протокола.
10:00 – 12:00 Ознакомление экспертов с нормативной документацией.
12:00 – 13:00 Обед
Ознакомление экспертов с конкурсной документацией.
Внесение 30% изменений в конкурсное задание.
13:00 – 15:00
Обсуждение критериев оценки, загрузка схемы оценки в ЦСО, блокировка.
Подписание протоколов.
Проведение специальной подготовки экспертов для обеспечения высокого
15:00 – 16:00 качества, профессионализма и соответствия процесса оценки правилам и
процедурам.
Распределение ролей главным экспертом на чемпионате: оценивающие эксперты;
эксперт ответственный за охрану труда на площадке проведения чемпионатного
16:00 – 16:30 мероприятия по компетенции; эксперт, ответственный за взаимодействие с
посетителями и представителями СМИ и др.
Подписание протоколов.
Формирование оценочных групп в ЦСО.
16:30 – 18:00 Подготовка ведомостей оценок.
Подписание протоколов.
18:00 – 19:00 Ужин
Д-1 / «14» сентября 2026 г.
09:00 – 09:30 Регистрация конкурсантов на площадке: сверка паспортов, подписание протокола.
09:30 – 10:00 Проведение инструктажа по охране труда, подписание протокола.
10:00 – 10:30 Ознакомление конкурсантов с нормативной документацией.
Ознакомление конкурсантов с актуализированной конкурсной документацией.
10:30 – 11:00
Подписание протоколов.
Жеребьевка рабочих мест.
11:00 – 12:00
Ознакомление конкурсантов с рабочими местами, тестирование оборудования.
12:00 – 13:00 Обед
Ознакомление конкурсантов с рабочими местами, тестирование оборудования.
13:00 – 17:00
Подписание протоколов.
17:00 – 19:00 Церемония открытия
19:30 – 20:30 Ужин
Д1 / «15» сентября 2026 г.
08:00 – 08:10 Прибытие на площадку экспертов и конкурсантов.
08:10 – 08:30 Проведение инструктажа по охране труда, подписание протокола.
Ознакомление конкурсантов с конкурсным заданием, подписание протокола.
08:30 – 09:00
Брифинг с Главным экспертом.
Выполнение конкурсантами задания, модуль А. Диагностика и отладка
09:00 – 10:00
электронных компонентов
Выполнение конкурсантами задания, модуль Б. Разработка и настройка
10:00 – 13:00
навигационных алгоритмов для РМК
13:00 – 14:00 Обед
Выполнение конкурсантами задания, модуль Б. Разработка и настройка
14:00 – 17:00
навигационных алгоритмов для РМК
Проведение экспертами оценки работ конкурсантов.
17:00 – 18:00
Занесение оценок в ЦСО.
18:00 – 19:00 Ужин
Д2 / «16» сентября 2026 г.
08:00 – 08:10 Прибытие на площадку экспертов и конкурсантов.
08:10 – 08:30 Проведение инструктажа по охране труда, подписание протокола.
Ознакомление конкурсантов с конкурсным заданием, подписание протокола.
08:30 – 09:00
Брифинг с Главным экспертом.
Выполнение конкурсантами задания, модуль В. Системы технического зрения с
09:00 – 14:00
использованием инструментов искусственного интеллекта
14:00 – 15:00 Обед
15:00 – 17:00 Выполнение конкурсантами задания, модуль Г. Разработка и тестирование
1
графического интерфейса (FMS-системы) для управления и визуализации работы
РМК и их группы
Проведение экспертами оценки работ конкурсантов.
17:00 – 18:00
Занесение оценок в ЦСО.
18:00 – 19:00 Ужин
Д3 / «17» сентября 2026 г.
08:00 – 08:10 Прибытие на площадку экспертов и конкурсантов.
08:10 – 08:30 Проведение инструктажа по охране труда, подписание протокола.
Ознакомление конкурсантов с конкурсным заданием, подписание протокола.
08:30 – 09:00
Брифинг с Главным экспертом.
Выполнение конкурсантами задания, модуль Г. Разработка и тестирование
09:00 – 11:00 графического интерфейса (FMS-системы) для управления и визуализации работы
РМК и их группы
Выполнение конкурсантами задания, модуль Д. Решение складского кейса
11:00 – 13:00
гетерогенной группой РМК
13:00 – 14:00 Обед
Выполнение конкурсантами задания, модуль Д. Решение складского кейса
14:00 – 18:00
гетерогенной группой РМК
18:00 – 19:00 Ужин
Проведение экспертами оценки работ конкурсантов.
Занесение оценок в ЦСО. Блокировка оценок.
19:00 – 21:00
Сверка ведомостей экспертами-наставниками.
Подведение итогов.
Д+1 / «18» сентября 2026 г.
09:00 – 14:00 Активности на площадке
14:00 – 16:00 Демонтаж площадки
16:00 – 19:00 Церемония награждения
19:00 – 20:00 Ужин
2
File diff suppressed because it is too large Load Diff
+215
View File
@@ -0,0 +1,215 @@
# Команды установки
Краткая шпаргалка без пояснений. Подробности и ограничения: [`FROM_ZERO.md`](FROM_ZERO.md).
## Windows PowerShell (администратор)
```powershell
winget install --id Microsoft.WindowsTerminal -e
winget install --id Microsoft.PowerShell -e
winget install --id Git.Git -e
winget install --id Microsoft.VisualStudioCode -e
winget install --id OpenJS.NodeJS.LTS -e
git --version
code --version
node --version
npm --version
wsl --install -d Ubuntu-24.04
```
## VS Code extensions
```bash
code --install-extension ms-python.python
code --install-extension ms-python.vscode-pylance
code --install-extension ms-iot.vscode-ros
code --install-extension ms-vscode.cpptools
code --install-extension ms-vscode.cmake-tools
code --install-extension redhat.vscode-yaml
code --install-extension bradlc.vscode-tailwindcss
code --install-extension dbaeumer.vscode-eslint
code --install-extension esbenp.prettier-vscode
code --install-extension ms-vscode-remote.remote-wsl
code --install-extension ms-vscode-remote.remote-ssh
```
## Ubuntu 24.04: база и VS Code
```bash
sudo apt update
sudo apt install -y \
git curl wget unzip ca-certificates gnupg locales software-properties-common \
build-essential python3 python3-pip python3-venv python3-dev \
mesa-utils libxcb-cursor0 libxcb-xinerama0
sudo snap install --classic code
git --version
python3 --version
code --version
glxinfo -B
```
## Git и SSH
```bash
git config --global user.name "Nikita Shilov"
git config --global user.email "YOUR_EMAIL"
git config --global init.defaultBranch main
ssh-keygen -t ed25519 -C "chvt-2026"
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
cat ~/.ssh/id_ed25519.pub
mkdir -p ~/scripts
git clone git@github.com:mavxa/mvch.git ~/scripts/mvch
git clone git@github.com:mavxa/mvch-docs.git ~/scripts/mvch-docs
git -C ~/scripts/mvch remote add gitea ssh://git@GITEA_HOST/USER/mvch.git
git -C ~/scripts/mvch push gitea --all
git -C ~/scripts/mvch push gitea --tags
git -C ~/scripts/mvch-docs remote add gitea ssh://git@GITEA_HOST/USER/mvch-docs.git
git -C ~/scripts/mvch-docs push gitea --all
git -C ~/scripts/mvch-docs push gitea --tags
```
## ROS 2 Jazzy
```bash
sudo locale-gen en_US en_US.UTF-8
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
export LANG=en_US.UTF-8
sudo add-apt-repository universe
sudo apt update
export ROS_APT_SOURCE_VERSION="$(curl -s https://api.github.com/repos/ros-infrastructure/ros-apt-source/releases/latest | grep -F 'tag_name' | awk -F\" '{print $4}')"
curl -L -o /tmp/ros2-apt-source.deb \
"https://github.com/ros-infrastructure/ros-apt-source/releases/download/${ROS_APT_SOURCE_VERSION}/ros2-apt-source_${ROS_APT_SOURCE_VERSION}.$(. /etc/os-release && echo ${UBUNTU_CODENAME:-${VERSION_CODENAME}})_all.deb"
sudo dpkg -i /tmp/ros2-apt-source.deb
sudo apt update
sudo apt install -y ros-jazzy-desktop ros-dev-tools \
ros-jazzy-rmw-fastrtps-cpp python3-colcon-common-extensions python3-rosdep
sudo rosdep init
rosdep update
source /opt/ros/jazzy/setup.bash
export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
```
## Webots
```bash
sudo mkdir -p /etc/apt/keyrings
sudo wget -q https://cyberbotics.com/Cyberbotics.asc \
-O /etc/apt/keyrings/Cyberbotics.asc
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/Cyberbotics.asc] https://cyberbotics.com/debian binary-amd64/" \
| sudo tee /etc/apt/sources.list.d/Cyberbotics.list
sudo apt update
sudo apt install -y webots
which webots
readlink -f "$(command -v webots)"
webots --version
```
## ROS-пакеты ЧВТ
```bash
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
git clone https://gitlab.mobird.dev/fms_group/ar_webots_fms_ros2.git
git clone https://gitlab.mobird.dev/fms_group/ar_arm95_moveit_config.git
git clone https://gitlab.mobird.dev/fms_group/ar_aruco_detect_ros2.git
git clone https://gitlab.mobird.dev/fms_group/ar_dual_lidar_merge_ros2.git
git clone https://gitlab.mobird.dev/fms_group/ar_nav_ros2.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash
rosdep install --from-paths src -y --ignore-src --rosdistro jazzy
colcon build --symlink-install
source ~/ros2_ws/install/setup.bash
```
## Python/YOLO
```bash
cd ~/scripts/mvch/module3
python3 -m venv --system-site-packages .venv
.venv/bin/python -m pip install -U pip
.venv/bin/pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
.venv/bin/pip install -r requirements.txt tqdm ultralytics-thop
.venv/bin/pip install --force-reinstall \
"numpy<2" "opencv-python<4.12" lark transforms3d
source /opt/ros/jazzy/setup.bash
source ~/ros2_ws/install/setup.bash
.venv/bin/python -c \
"import rclpy, cv_bridge, numpy, torch, ultralytics; print('Python/ROS/YOLO OK')"
```
## Bun, module4 и Node.js
```bash
curl -fsSL https://bun.com/install | bash
export BUN_INSTALL="$HOME/.bun"
export PATH="$BUN_INSTALL/bin:$PATH"
bun --version
cd ~/scripts/mvch/module4
bun install
bun run check
bun run build
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.7/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm install --lts
node --version
npm --version
```
## OpenCode (необязательно)
Ubuntu/Linux:
```bash
curl -fsSL https://opencode.ai/install | bash
source ~/.bashrc
opencode --version
```
Windows PowerShell:
```powershell
npm install -g opencode-ai
opencode --version
```
## Проверка окружения и топиков
```bash
cd ~/scripts/mvch-docs
./setup/check_environment.sh
source /opt/ros/jazzy/setup.bash
source ~/ros2_ws/install/setup.bash
export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
ros2 node list
ros2 topic list | sort
ros2 topic echo /RMC1/odometry --once
ros2 topic echo /RMC2/odometry --once
ros2 topic echo /RMC2/scan_front --once
```
## Git bundle на флешку
```bash
mkdir -p ~/chvt-backup
git -C ~/scripts/mvch bundle create ~/chvt-backup/mvch.bundle --all
git -C ~/scripts/mvch-docs bundle create ~/chvt-backup/mvch-docs.bundle --all
git bundle verify ~/chvt-backup/mvch.bundle
git bundle verify ~/chvt-backup/mvch-docs.bundle
```
+327
View File
@@ -0,0 +1,327 @@
# Подготовка рабочего места с нуля
Цель — за день настройки получить воспроизводимое рабочее место для физических РМК и Webots. Основная среда проекта: **Ubuntu 24.04 x86-64, ROS 2 Jazzy, Webots, Fast DDS, Python 3 и Git**.
Все команды без объяснений продублированы в [`COMMANDS.md`](COMMANDS.md).
## Сначала уточнить у эксперта
До установки и обновлений зафиксируйте:
1. Какая ОС и что уже установлено.
2. Нужно ли использовать готовый образ организаторов.
3. Какую версию Webots требует выданный пакет.
4. Адреса GitLab/локального Git, логин и доступ к интернету.
5. `ROS_DOMAIN_ID`, RMW и схему сети физических РМК.
6. Разрешённые программы, сайты и LLM.
Не обновляйте целиком уже работающее окружение непосредственно перед попыткой.
## Если выдали Windows
Windows используем как хост для редактора, Git и резервного доступа. Конкурсный ROS-стек из материалов рассчитан на Ubuntu 24.04. Нативную сборку всех пакетов под Windows нельзя считать равноценной без подтверждения организаторов.
Откройте PowerShell от администратора:
```powershell
winget install --id Microsoft.WindowsTerminal -e
winget install --id Microsoft.PowerShell -e
winget install --id Git.Git -e
winget install --id Microsoft.VisualStudioCode -e
winget install --id OpenJS.NodeJS.LTS -e
```
После установки закройте и снова откройте терминал:
```powershell
git --version
code --version
node --version
npm --version
```
Для вспомогательной Linux-среды можно установить Ubuntu 24.04 в WSL:
```powershell
wsl --install -d Ubuntu-24.04
```
После команды требуется перезапуск Windows. WSL подходит для Git, Python, Bun и редактирования, но DDS discovery, multicast, USB-камера и графический Webots могут отличаться от обычного Ubuntu. Для физического заезда предпочтительны нативный Ubuntu либо выданная Ubuntu VM с bridged-сетью и проверенным OpenGL.
Если используется Ubuntu VM, выделите не менее 4 CPU, 8 ГБ RAM и 50 ГБ диска. Включайте 3D-ускорение только после проверки стабильности конкретного гипервизора.
## Если выдали Ubuntu 24.04
### Базовые программы
```bash
sudo apt update
sudo apt install -y \
git curl wget unzip ca-certificates gnupg locales software-properties-common \
build-essential python3 python3-pip python3-venv python3-dev \
mesa-utils libxcb-cursor0 libxcb-xinerama0
```
Проверка GPU:
```bash
glxinfo -B
```
В `OpenGL renderer` не должно быть `llvmpipe`, если Webots должен работать с аппаратным ускорением.
### Visual Studio Code
Короткий официальный вариант для Ubuntu:
```bash
sudo snap install --classic code
code --version
```
Snap здесь используется только для VS Code. Webots устанавливайте через `.deb`/APT: Snap-версия Webots уже создавала проблемы с ROS и Qt.
Минимальные расширения:
```bash
code --install-extension ms-python.python
code --install-extension ms-python.vscode-pylance
code --install-extension ms-iot.vscode-ros
code --install-extension ms-vscode.cpptools
code --install-extension ms-vscode.cmake-tools
code --install-extension redhat.vscode-yaml
code --install-extension bradlc.vscode-tailwindcss
code --install-extension dbaeumer.vscode-eslint
code --install-extension esbenp.prettier-vscode
```
Для Windows/WSL дополнительно:
```powershell
code --install-extension ms-vscode-remote.remote-wsl
code --install-extension ms-vscode-remote.remote-ssh
```
### Git и доступ к репозиториям
```bash
git config --global user.name "Nikita Shilov"
git config --global user.email "YOUR_EMAIL"
git config --global init.defaultBranch main
ssh-keygen -t ed25519 -C "chvt-2026"
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
cat ~/.ssh/id_ed25519.pub
```
Добавьте **публичный** ключ в GitHub/Gitea. Приватный `~/.ssh/id_ed25519` никуда не отправляйте и не коммитьте.
```bash
mkdir -p ~/scripts
git clone git@github.com:mavxa/mvch.git ~/scripts/mvch
git clone git@github.com:mavxa/mvch-docs.git ~/scripts/mvch-docs
```
Если GitHub недоступен, замените URL на зеркало Gitea. После клонирования код полностью локален; интернет для `git status`, веток и коммитов не нужен.
Добавление зеркала к уже клонированному репозиторию:
```bash
git remote add gitea ssh://git@GITEA_HOST/USER/mvch.git
git push gitea --all
git push gitea --tags
```
Для `mvch-docs` повторите команды в его каталоге с URL репозитория `mvch-docs`.
### ROS 2 Jazzy
```bash
sudo locale-gen en_US en_US.UTF-8
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
export LANG=en_US.UTF-8
sudo add-apt-repository universe
sudo apt update
export ROS_APT_SOURCE_VERSION="$(curl -s https://api.github.com/repos/ros-infrastructure/ros-apt-source/releases/latest | grep -F 'tag_name' | awk -F\" '{print $4}')"
curl -L -o /tmp/ros2-apt-source.deb \
"https://github.com/ros-infrastructure/ros-apt-source/releases/download/${ROS_APT_SOURCE_VERSION}/ros2-apt-source_${ROS_APT_SOURCE_VERSION}.$(. /etc/os-release && echo ${UBUNTU_CODENAME:-${VERSION_CODENAME}})_all.deb"
sudo dpkg -i /tmp/ros2-apt-source.deb
sudo apt update
sudo apt install -y ros-jazzy-desktop ros-dev-tools \
ros-jazzy-rmw-fastrtps-cpp python3-colcon-common-extensions python3-rosdep
```
Инициализация `rosdep` выполняется один раз:
```bash
sudo rosdep init
rosdep update
```
Если `rosdep init` пишет, что файл источников уже существует, повторять команду не нужно.
```bash
source /opt/ros/jazzy/setup.bash
export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
ros2 --help
```
### Webots
```bash
sudo mkdir -p /etc/apt/keyrings
sudo wget -q https://cyberbotics.com/Cyberbotics.asc \
-O /etc/apt/keyrings/Cyberbotics.asc
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/Cyberbotics.asc] https://cyberbotics.com/debian binary-amd64/" \
| sudo tee /etc/apt/sources.list.d/Cyberbotics.list
sudo apt update
sudo apt install -y webots
which webots
readlink -f "$(command -v webots)"
webots --version
```
Не задавайте `WEBOTS_HOME=/snap/webots/...` для APT-версии. Сначала проверьте реальный путь.
### Пакеты симулятора и управления
```bash
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
git clone https://gitlab.mobird.dev/fms_group/ar_webots_fms_ros2.git
git clone https://gitlab.mobird.dev/fms_group/ar_arm95_moveit_config.git
git clone https://gitlab.mobird.dev/fms_group/ar_aruco_detect_ros2.git
git clone https://gitlab.mobird.dev/fms_group/ar_dual_lidar_merge_ros2.git
git clone https://gitlab.mobird.dev/fms_group/ar_nav_ros2.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash
rosdep install --from-paths src -y --ignore-src --rosdistro jazzy
colcon build --symlink-install
source ~/ros2_ws/install/setup.bash
```
Если закрытый GitLab не открывается, это вопрос доступа организаторов. Не подменяйте пакеты случайными форками.
Добавить окружение в Bash можно после успешной ручной проверки:
```bash
printf '%s\n' \
'source /opt/ros/jazzy/setup.bash' \
'source ~/ros2_ws/install/setup.bash' \
'export RMW_IMPLEMENTATION=rmw_fastrtps_cpp' \
>> ~/.bashrc
```
### Python, YOLO и ROS
```bash
cd ~/scripts/mvch/module3
python3 -m venv --system-site-packages .venv
.venv/bin/python -m pip install -U pip
.venv/bin/pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
.venv/bin/pip install -r requirements.txt tqdm ultralytics-thop
.venv/bin/pip install --force-reinstall \
"numpy<2" "opencv-python<4.12" lark transforms3d
```
Проверка:
```bash
source /opt/ros/jazzy/setup.bash
source ~/ros2_ws/install/setup.bash
~/scripts/mvch/module3/.venv/bin/python -c \
"import rclpy, cv_bridge, numpy, torch, ultralytics; print('Python/ROS/YOLO OK')"
```
CPU-сборки достаточно для запуска и короткой проверки. CUDA/ROCm устанавливайте только под фактическую видеокарту по официальному селектору PyTorch.
### Bun и Node.js
Для `module4` нужен Bun:
```bash
sudo apt install -y unzip
curl -fsSL https://bun.com/install | bash
export BUN_INSTALL="$HOME/.bun"
export PATH="$BUN_INSTALL/bin:$PATH"
bun --version
cd ~/scripts/mvch/module4
bun install
bun run check
bun run build
```
Node.js для этого проекта не обязателен, но полезен как запасной runtime и для npm-инструментов:
```bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.7/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm install --lts
node --version
npm --version
```
### OpenCode — только если разрешён и нужен
Linux/Ubuntu:
```bash
curl -fsSL https://opencode.ai/install | bash
opencode --version
```
Windows после установки Node.js:
```powershell
npm install -g opencode-ai
opencode --version
```
Не записывайте API-ключи в репозитории, README или историю терминала. Способ авторизации выбирайте по правилам площадки.
## День настройки физического оборудования
Делайте проверки по слоям:
1. `ip addr`, `ip route`, доступность адресов РМК.
2. `ROS_DOMAIN_ID` и `RMW_IMPLEMENTATION` по данным эксперта.
3. `ros2 topic list | sort` и `ros2 node list`.
4. Отдельно проверить `/RMC1/odometry`, `/RMC2/odometry`, лидары, камеры и TF.
5. На вывешенных колёсах кратко подать малую скорость и немедленно STOP.
6. Проверить лифт RMC2 без стеллажа.
7. Проверить ARM95 сначала без детали и на безопасной высоте.
8. Собрать датасет только выданной/разрешённой камерой и сделать dry-run детектора.
9. Зафиксировать реальные топики, фреймы, размеры, точки и параметры в отдельном локальном коммите.
10. Сделать контрольный прогон и офлайн-бэкап.
Не начинайте с полного модуля Д: сначала независимо подтвердите каждую платформу, лифт, камеру, детектор и манипулятор.
## Защита от блокировки GitHub
Основной вариант — зеркало на доступной Gitea. Дополнительно сделайте Git bundle обоих репозиториев:
```bash
mkdir -p ~/chvt-backup
git -C ~/scripts/mvch bundle create ~/chvt-backup/mvch.bundle --all
git -C ~/scripts/mvch-docs bundle create ~/chvt-backup/mvch-docs.bundle --all
git bundle verify ~/chvt-backup/mvch.bundle
git bundle verify ~/chvt-backup/mvch-docs.bundle
```
Проверка восстановления:
```bash
git clone ~/chvt-backup/mvch.bundle /tmp/mvch-restore-test
git -C /tmp/mvch-restore-test log -1 --oneline
```
Bundle содержит историю Git и отслеживаемые веса модели, но не содержит `.venv`, `node_modules`, системные пакеты и секреты. На флешку отдельно сохраните установщик/образ Ubuntu, нужную версию Webots, экспорт датасета и при необходимости wheel/cache зависимостей.
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env bash
set -u
warnings=0
check_command() {
if command -v "$1" >/dev/null 2>&1; then
printf '[OK] %s: %s\n' "$1" "$(command -v "$1")"
else
printf '[WARN] команда %s не найдена\n' "$1"
warnings=$((warnings + 1))
fi
}
if [[ -r /etc/os-release ]]; then
# shellcheck disable=SC1091
source /etc/os-release
printf '[INFO] OS: %s %s\n' "${NAME:-unknown}" "${VERSION_ID:-unknown}"
if [[ "${ID:-}" == ubuntu && "${VERSION_ID:-}" == 24.04 ]]; then
:
else
printf '[WARN] официальное окружение задания рассчитано на Ubuntu 24.04\n'
warnings=$((warnings + 1))
fi
fi
for command_name in git python3 code ros2 colcon webots bun; do
check_command "$command_name"
done
for optional_name in node npm opencode; do
if command -v "$optional_name" >/dev/null 2>&1; then
printf '[OK] необязательная команда %s: %s\n' "$optional_name" "$(command -v "$optional_name")"
else
printf '[INFO] необязательная команда %s не установлена\n' "$optional_name"
fi
done
printf '[INFO] RMW_IMPLEMENTATION=%s\n' "${RMW_IMPLEMENTATION:-не задано}"
printf '[INFO] ROS_DOMAIN_ID=%s\n' "${ROS_DOMAIN_ID:-не задано}"
if [[ -r /opt/ros/jazzy/setup.bash ]]; then
set +u
# shellcheck disable=SC1091
source /opt/ros/jazzy/setup.bash
set -u
printf '[OK] ROS 2 Jazzy setup найден\n'
else
printf '[WARN] /opt/ros/jazzy/setup.bash не найден\n'
warnings=$((warnings + 1))
fi
workspace_path="${ROS_WORKSPACE:-$HOME/ros2_ws}"
if [[ -r "$workspace_path/install/setup.bash" ]]; then
set +u
# shellcheck disable=SC1090
source "$workspace_path/install/setup.bash"
set -u
printf '[OK] workspace: %s\n' "$workspace_path"
else
printf '[WARN] workspace не собран: %s\n' "$workspace_path"
warnings=$((warnings + 1))
fi
if command -v ros2 >/dev/null 2>&1; then
for package_name in ar_webots_fms_ros2 ar_arm95_moveit_config ar_aruco_detect_ros2; do
if ros2 pkg prefix "$package_name" >/dev/null 2>&1; then
printf '[OK] ROS-пакет: %s\n' "$package_name"
else
printf '[WARN] ROS-пакет не найден: %s\n' "$package_name"
warnings=$((warnings + 1))
fi
done
fi
if python3 -c 'import rclpy, cv_bridge' >/dev/null 2>&1; then
printf '[OK] Python: rclpy и cv_bridge импортируются\n'
else
printf '[WARN] Python не импортирует rclpy/cv_bridge\n'
warnings=$((warnings + 1))
fi
if command -v glxinfo >/dev/null 2>&1; then
glxinfo -B 2>/dev/null | sed -n '/OpenGL vendor/p;/OpenGL renderer/p'
fi
printf '\nИтог: %d предупреждений. WARN не всегда означает поломку, но проверьте его до заезда.\n' "$warnings"