refactor(module2): keep only target argument
This commit is contained in:
@@ -73,7 +73,7 @@ cd ~/mvch
|
|||||||
git pull --ff-only
|
git pull --ff-only
|
||||||
source /opt/ros/jazzy/setup.bash
|
source /opt/ros/jazzy/setup.bash
|
||||||
source ~/ros2_ws/install/setup.bash
|
source ~/ros2_ws/install/setup.bash
|
||||||
python3 module2/module_b.py --target 14 --sim
|
python3 module2/module_b.py --target 14
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Поставьте РМК-2 центром над любой меткой. Код сам определит стартовый ID и
|
1. Поставьте РМК-2 центром над любой меткой. Код сам определит стартовый ID и
|
||||||
@@ -86,31 +86,25 @@ python3 module2/module_b.py --target 14 --sim
|
|||||||
исключит ребро и выведет `ROUTE_REPLANNED`.
|
исключит ребро и выведет `ROUTE_REPLANNED`.
|
||||||
6. Успешный конец — `MISSION_FINISHED`. `Ctrl+C` публикует нулевую скорость.
|
6. Успешный конец — `MISSION_FINISHED`. `Ctrl+C` публикует нулевую скорость.
|
||||||
|
|
||||||
На физическом РМК-2 команда такая же, только `--sim` можно не писать:
|
На физическом РМК-2 команда такая же:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 module2/module_b.py --target 14
|
python3 module2/module_b.py --target 14
|
||||||
```
|
```
|
||||||
|
|
||||||
Без `--sim` используется площадочное поле 5×5, ID 0–24. Флаг `--sim`
|
Единственный аргумент — целевой ID:
|
||||||
включает старый Webots 6×6, ID 0–35. Закрытые ячейки передаются через
|
|
||||||
`--blocked`:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 module2/module_b.py --target 14 --blocked 7 8 13 --sim
|
python3 module2/module_b.py --target 14
|
||||||
```
|
```
|
||||||
|
|
||||||
Основные параметры:
|
Остальные настройки находятся в начале `module2/module_b.py`:
|
||||||
|
|
||||||
- `--target` — целевой ArUco ID;
|
- `ROWS`, `COLUMNS` — площадка 5×5; для старого симулятора поставить 6 и 6;
|
||||||
- `--blocked` — закрытые ячейки;
|
- `SPACING` — шаг между метками;
|
||||||
- `--sim` — профиль симулятора 6×6;
|
- `BLOCKED` — закрытые ID, например `{7, 8, 13}`;
|
||||||
- `--rows`, `--columns`, `--spacing` — геометрия сетки;
|
- `MAX_SPEED`, `MAX_ANGULAR` — скорости;
|
||||||
- `--speed` — линейная скорость, по умолчанию 0.35 м/с, предел RMC2 0.5 м/с;
|
- `STOP_DISTANCE`, `TOLERANCE`, `SCAN_TOPIC` — lidar и точность остановки.
|
||||||
- `--angular` — скорость поворота, по умолчанию 0.6 рад/с;
|
|
||||||
- `--stop-distance` — дистанция срабатывания объезда, по умолчанию 0.65 м;
|
|
||||||
- `--tolerance` — допустимое смещение от центра ArUco, по умолчанию 0.07 м;
|
|
||||||
- `--scan-topic` — передний лидар, по умолчанию `/RMC2/scan_front`.
|
|
||||||
|
|
||||||
## Как устроен код
|
## Как устроен код
|
||||||
|
|
||||||
@@ -126,13 +120,7 @@ odometry, а на каждой следующей метке снова корр
|
|||||||
workspace RMC2 управляется через `/RMC2/cmd_vel`; Nav2 настроен для RMC1. Поэтому
|
workspace RMC2 управляется через `/RMC2/cmd_vel`; Nav2 настроен для RMC1. Поэтому
|
||||||
модуль Б напрямую использует ArUco, odometry и `/RMC2/scan_front`.
|
модуль Б напрямую использует ArUco, odometry и `/RMC2/scan_front`.
|
||||||
|
|
||||||
## Проверки и логи
|
## Логи
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 module2/module_b.py --target 14 --start 0 --dry-run
|
|
||||||
python3 module2/module_b.py --target 8 --start 0 --sim --dry-run
|
|
||||||
python3 module2/module_b.py --target 24 --start 6 --blocked 7 8 --dry-run
|
|
||||||
```
|
|
||||||
|
|
||||||
`module2/reports/module_b_*.jsonl` содержит обнаруженные маркеры, оба маршрута,
|
`module2/reports/module_b_*.jsonl` содержит обнаруженные маркеры, оба маршрута,
|
||||||
препятствия, достигнутые точки и чистое время движения. Паузы на установку
|
препятствия, достигнутые точки и чистое время движения. Паузы на установку
|
||||||
|
|||||||
+35
-77
@@ -16,6 +16,8 @@ MAX_SPEED = 0.35
|
|||||||
MAX_ANGULAR = 0.60
|
MAX_ANGULAR = 0.60
|
||||||
STOP_DISTANCE = 0.65
|
STOP_DISTANCE = 0.65
|
||||||
TOLERANCE = 0.07
|
TOLERANCE = 0.07
|
||||||
|
SCAN_TOPIC = "/RMC2/scan_front"
|
||||||
|
BLOCKED = set()
|
||||||
|
|
||||||
|
|
||||||
def clamp(value, low, high):
|
def clamp(value, low, high):
|
||||||
@@ -51,14 +53,14 @@ def inverse(pose):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def marker_pose(marker, columns, spacing):
|
def marker_pose(marker):
|
||||||
row, column = divmod(marker, columns)
|
row, column = divmod(marker, COLUMNS)
|
||||||
return -row * spacing, column * spacing, 0.0
|
return -row * SPACING, column * SPACING, 0.0
|
||||||
|
|
||||||
|
|
||||||
def shortest_path(start, goal, rows, columns, blocked, forbidden=()):
|
def shortest_path(start, goal, forbidden=()):
|
||||||
if start in blocked or goal in blocked:
|
if start in BLOCKED or goal in BLOCKED:
|
||||||
raise RuntimeError("Старт или цель находятся в закрытой ячейке")
|
raise RuntimeError("невозможная цель")
|
||||||
|
|
||||||
forbidden = {frozenset(edge) for edge in forbidden}
|
forbidden = {frozenset(edge) for edge in forbidden}
|
||||||
search = deque([start])
|
search = deque([start])
|
||||||
@@ -68,24 +70,24 @@ def shortest_path(start, goal, rows, columns, blocked, forbidden=()):
|
|||||||
current = search.popleft()
|
current = search.popleft()
|
||||||
if current == goal:
|
if current == goal:
|
||||||
break
|
break
|
||||||
row, column = divmod(current, columns)
|
row, column = divmod(current, COLUMNS)
|
||||||
for next_row, next_column in (
|
for next_row, next_column in (
|
||||||
(row - 1, column),
|
(row - 1, column),
|
||||||
(row + 1, column),
|
(row + 1, column),
|
||||||
(row, column - 1),
|
(row, column - 1),
|
||||||
(row, column + 1),
|
(row, column + 1),
|
||||||
):
|
):
|
||||||
if not (0 <= next_row < rows and 0 <= next_column < columns):
|
if not (0 <= next_row < ROWS and 0 <= next_column < COLUMNS):
|
||||||
continue
|
continue
|
||||||
candidate = next_row * columns + next_column
|
candidate = next_row * COLUMNS + next_column
|
||||||
edge = frozenset((current, candidate))
|
edge = frozenset((current, candidate))
|
||||||
if candidate in blocked or candidate in parent or edge in forbidden:
|
if candidate in BLOCKED or candidate in parent or edge in forbidden:
|
||||||
continue
|
continue
|
||||||
parent[candidate] = current
|
parent[candidate] = current
|
||||||
search.append(candidate)
|
search.append(candidate)
|
||||||
|
|
||||||
if goal not in parent:
|
if goal not in parent:
|
||||||
raise RuntimeError(f"Нет маршрута {start} -> {goal}")
|
raise RuntimeError(f"нет маршрута {start} к {goal}")
|
||||||
|
|
||||||
route = []
|
route = []
|
||||||
current = goal
|
current = goal
|
||||||
@@ -98,51 +100,11 @@ def shortest_path(start, goal, rows, columns, blocked, forbidden=()):
|
|||||||
def arguments():
|
def arguments():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--target", type=int, required=True)
|
parser.add_argument("--target", type=int, required=True)
|
||||||
parser.add_argument("--rows", type=int)
|
return parser.parse_args().target
|
||||||
parser.add_argument("--columns", type=int)
|
|
||||||
parser.add_argument("--spacing", type=float, default=SPACING)
|
|
||||||
parser.add_argument("--blocked", type=int, nargs="*", default=[])
|
|
||||||
parser.add_argument("--speed", type=float, default=MAX_SPEED)
|
|
||||||
parser.add_argument("--angular", type=float, default=MAX_ANGULAR)
|
|
||||||
parser.add_argument("--stop-distance", type=float, default=STOP_DISTANCE)
|
|
||||||
parser.add_argument("--tolerance", type=float, default=TOLERANCE)
|
|
||||||
parser.add_argument("--scan-topic", default="/RMC2/scan_front")
|
|
||||||
parser.add_argument("--sim", action="store_true", help="Симулятор с сеткой 6x6")
|
|
||||||
parser.add_argument("--dry-run", action="store_true")
|
|
||||||
parser.add_argument("--start", type=int, default=0)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.rows is None:
|
|
||||||
args.rows = 6 if args.sim else ROWS
|
|
||||||
if args.columns is None:
|
|
||||||
args.columns = 6 if args.sim else COLUMNS
|
|
||||||
|
|
||||||
count = args.rows * args.columns
|
|
||||||
if args.rows < 1 or args.columns < 1:
|
|
||||||
parser.error("rows и columns должны быть больше нуля")
|
|
||||||
if not 0 <= args.target < count:
|
|
||||||
parser.error(f"target должен быть в диапазоне 0..{count - 1}")
|
|
||||||
if args.spacing <= 0 or args.stop_distance <= 0 or args.tolerance <= 0:
|
|
||||||
parser.error("расстояния должны быть больше нуля")
|
|
||||||
if not 0 < args.speed <= 0.5:
|
|
||||||
parser.error("скорость RMC2 должна быть в диапазоне 0..0.5 м/с")
|
|
||||||
if not 0 < args.angular <= 1.0:
|
|
||||||
parser.error("angular должна быть в диапазоне 0..1.0 рад/с")
|
|
||||||
if args.dry_run and not 0 <= args.start < count:
|
|
||||||
parser.error(f"start должен быть в диапазоне 0..{count - 1}")
|
|
||||||
return args
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = arguments()
|
target_id = arguments()
|
||||||
blocked = set(args.blocked)
|
|
||||||
|
|
||||||
if args.dry_run:
|
|
||||||
there = shortest_path(args.start, args.target, args.rows, args.columns, blocked)
|
|
||||||
back = shortest_path(args.target, args.start, args.rows, args.columns, blocked)
|
|
||||||
print("К ЦЕЛИ:", " -> ".join(map(str, there)))
|
|
||||||
print("НА СТАРТ:", " -> ".join(map(str, back)))
|
|
||||||
return
|
|
||||||
|
|
||||||
import rclpy
|
import rclpy
|
||||||
from geometry_msgs.msg import Twist
|
from geometry_msgs.msg import Twist
|
||||||
@@ -169,7 +131,7 @@ def main():
|
|||||||
Odometry, "/RMC2/odometry", self.on_odom, qos_profile_sensor_data
|
Odometry, "/RMC2/odometry", self.on_odom, qos_profile_sensor_data
|
||||||
)
|
)
|
||||||
self.create_subscription(
|
self.create_subscription(
|
||||||
LaserScan, args.scan_topic, self.on_scan, qos_profile_sensor_data
|
LaserScan, SCAN_TOPIC, self.on_scan, qos_profile_sensor_data
|
||||||
)
|
)
|
||||||
self.create_subscription(
|
self.create_subscription(
|
||||||
String, "/RMC2/camera_bottom/aruco_id", self.on_marker, 10
|
String, "/RMC2/camera_bottom/aruco_id", self.on_marker, 10
|
||||||
@@ -240,7 +202,7 @@ def main():
|
|||||||
if not match:
|
if not match:
|
||||||
return
|
return
|
||||||
marker = int(match.group())
|
marker = int(match.group())
|
||||||
if not 0 <= marker < args.rows * args.columns:
|
if not 0 <= marker < ROWS * COLUMNS:
|
||||||
return
|
return
|
||||||
if marker != self.marker:
|
if marker != self.marker:
|
||||||
self.log("MARKER", marker=marker)
|
self.log("MARKER", marker=marker)
|
||||||
@@ -274,7 +236,7 @@ def main():
|
|||||||
yaw(transform.rotation),
|
yaw(transform.rotation),
|
||||||
)
|
)
|
||||||
self.map_from_odom = compose(
|
self.map_from_odom = compose(
|
||||||
marker_pose(self.marker, args.columns, args.spacing), inverse(odom_marker)
|
marker_pose(self.marker), inverse(odom_marker)
|
||||||
)
|
)
|
||||||
self.pose = compose(self.map_from_odom, self.odom)
|
self.pose = compose(self.map_from_odom, self.odom)
|
||||||
self.marker_error = math.hypot(
|
self.marker_error = math.hypot(
|
||||||
@@ -301,7 +263,7 @@ def main():
|
|||||||
while rclpy.ok():
|
while rclpy.ok():
|
||||||
self.update()
|
self.update()
|
||||||
self.stop()
|
self.stop()
|
||||||
if self.pose is not None and self.marker_error <= args.tolerance:
|
if self.pose is not None and self.marker_error <= TOLERANCE:
|
||||||
self.current = self.marker
|
self.current = self.marker
|
||||||
self.log("START_FOUND", marker=self.current)
|
self.log("START_FOUND", marker=self.current)
|
||||||
return self.current
|
return self.current
|
||||||
@@ -310,11 +272,11 @@ def main():
|
|||||||
return (
|
return (
|
||||||
self.marker == marker
|
self.marker == marker
|
||||||
and time.monotonic() - self.marker_seen < 0.8
|
and time.monotonic() - self.marker_seen < 0.8
|
||||||
and self.marker_error <= args.tolerance
|
and self.marker_error <= TOLERANCE
|
||||||
)
|
)
|
||||||
|
|
||||||
def drive_to(self, marker, check_obstacle=True):
|
def drive_to(self, marker, check_obstacle=True):
|
||||||
target = marker_pose(marker, args.columns, args.spacing)
|
target = marker_pose(marker)
|
||||||
while rclpy.ok():
|
while rclpy.ok():
|
||||||
self.update()
|
self.update()
|
||||||
if self.pose is None:
|
if self.pose is None:
|
||||||
@@ -328,18 +290,16 @@ def main():
|
|||||||
dy = target[1] - self.pose[1]
|
dy = target[1] - self.pose[1]
|
||||||
distance = math.hypot(dx, dy)
|
distance = math.hypot(dx, dy)
|
||||||
error = wrap(math.atan2(dy, dx) - self.pose[2])
|
error = wrap(math.atan2(dy, dx) - self.pose[2])
|
||||||
linear = 0.0 if abs(error) > 0.16 else min(args.speed, max(0.04, 0.7 * distance))
|
linear = 0.0 if abs(error) > 0.16 else min(MAX_SPEED, max(0.04, 0.7 * distance))
|
||||||
angular = clamp(1.4 * error, -args.angular, args.angular)
|
angular = clamp(1.4 * error, -MAX_ANGULAR, MAX_ANGULAR)
|
||||||
|
|
||||||
if check_obstacle and linear > 0 and self.front < args.stop_distance:
|
if check_obstacle and linear > 0 and self.front < STOP_DISTANCE:
|
||||||
self.stop()
|
self.stop()
|
||||||
return False
|
return False
|
||||||
self.send(linear, angular)
|
self.send(linear, angular)
|
||||||
|
|
||||||
def follow(self, goal, forbidden):
|
def follow(self, goal, forbidden):
|
||||||
route = shortest_path(
|
route = shortest_path(self.current, goal, forbidden)
|
||||||
self.current, goal, args.rows, args.columns, blocked, forbidden
|
|
||||||
)
|
|
||||||
index = 1
|
index = 1
|
||||||
while index < len(route):
|
while index < len(route):
|
||||||
next_marker = route[index]
|
next_marker = route[index]
|
||||||
@@ -358,15 +318,13 @@ def main():
|
|||||||
)
|
)
|
||||||
print(f"Препятствие перед {next_marker}. Возвращаюсь к {self.current}.")
|
print(f"Препятствие перед {next_marker}. Возвращаюсь к {self.current}.")
|
||||||
self.drive_to(self.current, check_obstacle=False)
|
self.drive_to(self.current, check_obstacle=False)
|
||||||
route = shortest_path(
|
route = shortest_path(self.current, goal, forbidden)
|
||||||
self.current, goal, args.rows, args.columns, blocked, forbidden
|
|
||||||
)
|
|
||||||
index = 1
|
index = 1
|
||||||
show_route(self, "НОВЫЙ МАРШРУТ", route, "ROUTE_REPLANNED")
|
show_route(self, "НОВЫЙ МАРШРУТ", route, "ROUTE_REPLANNED")
|
||||||
return route
|
return route
|
||||||
|
|
||||||
def show_route(rover, title, route, event):
|
def show_route(rover, title, route, event):
|
||||||
print(f"{title}: {' -> '.join(map(str, route))}")
|
print(f"{title}: {' путь: '.join(map(str, route))}")
|
||||||
rover.log(event, markers=route)
|
rover.log(event, markers=route)
|
||||||
|
|
||||||
rclpy.init(signal_handler_options=SignalHandlerOptions.NO)
|
rclpy.init(signal_handler_options=SignalHandlerOptions.NO)
|
||||||
@@ -374,25 +332,25 @@ def main():
|
|||||||
exit_code = 0
|
exit_code = 0
|
||||||
try:
|
try:
|
||||||
start = rover.wait_start()
|
start = rover.wait_start()
|
||||||
route_there = shortest_path(start, args.target, args.rows, args.columns, blocked)
|
route_there = shortest_path(start, target_id)
|
||||||
route_back = shortest_path(args.target, start, args.rows, args.columns, blocked)
|
route_back = shortest_path(target_id, start)
|
||||||
|
|
||||||
print()
|
print()
|
||||||
show_route(rover, "МАРШРУТ К ЦЕЛИ", route_there, "ROUTE_TO_TARGET")
|
show_route(rover, "МАРШРУТ К ЦЕЛИ", route_there, "ROUTE_TO_TARGET")
|
||||||
show_route(rover, "МАРШРУТ НА СТАРТ", route_back, "ROUTE_TO_START")
|
show_route(rover, "МАРШРУТ НА СТАРТ", route_back, "ROUTE_TO_START")
|
||||||
input("\nEnter — начать движение: ")
|
input("\nenter - начать движение:")
|
||||||
|
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
rover.log("MOVEMENT_START", markers=route_there)
|
rover.log("MOVEMENT_START", markers=route_there)
|
||||||
rover.follow(args.target, set())
|
rover.follow(target_id, set())
|
||||||
first_time = time.monotonic() - started
|
first_time = time.monotonic() - started
|
||||||
rover.log("MOVEMENT_FINISHED", marker=args.target, seconds=round(first_time, 2))
|
rover.log("MOVEMENT_FINISHED", marker=target_id, seconds=round(first_time, 2))
|
||||||
rover.log("TARGET_REACHED", marker=args.target, seconds=round(first_time, 2))
|
rover.log("TARGET_REACHED", marker=target_id, seconds=round(first_time, 2))
|
||||||
|
|
||||||
input("\nЦель достигнута. Поставьте препятствие и нажмите Enter: ")
|
input("\nцель достигнута. ожидание препятствие и enter: ")
|
||||||
rover.leg = "return"
|
rover.leg = "return"
|
||||||
show_route(rover, "МАРШРУТ НА СТАРТ", route_back, "ROUTE_TO_START")
|
show_route(rover, "МАРШРУТ НА СТАРТ", route_back, "ROUTE_TO_START")
|
||||||
input("Enter — начать возврат: ")
|
input("Enter - начать возврат: ")
|
||||||
|
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
rover.log("MOVEMENT_START", markers=route_back)
|
rover.log("MOVEMENT_START", markers=route_back)
|
||||||
|
|||||||
Reference in New Issue
Block a user