Merge branch 'dev'

This commit is contained in:
Даниил Грабарь
2026-08-12 20:30:18 +03:00
7 changed files with 708 additions and 455 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,184 @@
# Ошибка SSL при установке
## Описание проблемы
При выполнении `cobot setup` или `rosdep update` может возникать ошибка SSL-рукопожатия при попытке загрузить индексы зависимостей ROS с серверов GitHub.
**Возможные причины:**
- Сетевые ограничения (корпоративный брандмауэр, интернет-провайдер)
- Блокировка GitHub на уровне маршрутизатора или провайдера
- Проблемы с DNS-разрешением `raw.githubusercontent.com`
- Ограниченный доступ к TLS-соединениям (Deep Packet Inspection)
## Симптомы
=== "cobot setup"
```
[rosdep] Initializing rosdep...
ERROR: unable to process source [https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/base.yaml]:
<urlopen error _ssl.c:983: The handshake operation timed out>
ERROR: unable to process source [https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/python.yaml]:
<urlopen error _ssl.c:983: The handshake operation timed out>
ERROR: Not all sources were able to be updated.
```
=== "rosdep update"
```
/usr/bin/rosdep:6: DeprecationWarning: pkg_resources is deprecated as an API.
from pkg_resources import load_entry_point
ERROR: unable to process source [https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/base.yaml]:
<urlopen error _ssl.c:983: The handshake operation timed out> (https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/base.yaml)
ERROR: unable to process source [https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/python.yaml]:
<urlopen error _ssl.c:983: The handshake operation timed out> (https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/python.yaml)
ERROR: Not all sources were able to be updated.
[[[
ERROR: unable to process source [https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/base.yaml]:
<urlopen error _ssl.c:983: The handshake operation timed out> (https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/base.yaml)
ERROR: unable to process source [https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/python.yaml]:
<urlopen error _ssl.c:983: The handshake operation timed out> (https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/python.yaml)
```
---
## Решение: VPN через WireGuard
Рекомендуемый способ обойти сетевые ограничения — поднять WireGuard VPN-туннель.
По настройке **WireGuard-сервера** обратитесь к [официальной документации WireGuard](https://www.wireguard.com/quickstart/) или документации вашего облачного провайдера.
Ниже приведена настройка **клиентской части** на рабочей машине.
---
### 1. Установка WireGuard
```bash
sudo apt update && sudo apt install -y wireguard-tools
```
---
### 2. Конфигурация клиента
Создайте файл конфигурации:
```bash
sudo nano /etc/wireguard/wg0.conf
```
Добавьте следующее содержимое, подставив данные вашего сервера:
```ini
[Interface]
# Приватный ключ клиента (генерируется командой: wg genkey)
PrivateKey = <ВАШ_ПРИВАТНЫЙ_КЛЮЧ>
# IP-адрес клиента в VPN-сети
Address = 10.0.0.2/24
# DNS-серверы (опционально)
DNS = 8.8.8.8, 1.1.1.1
[Peer]
# Публичный ключ WireGuard-сервера
PublicKey = <ПУБЛИЧНЫЙ_КЛЮЧ_СЕРВЕРА>
# Адрес и UDP-порт сервера
Endpoint = <IP_СЕРВЕРА>:51820
# Маршрутизировать весь трафик через VPN
AllowedIPs = 0.0.0.0/0
# Keepalive для клиентов за NAT
PersistentKeepalive = 25
```
!!! tip "Генерация ключей"
Если у вас ещё нет ключевой пары, сгенерируйте её:
```bash
# Приватный ключ
wg genkey | tee privatekey
# Публичный ключ (передайте администратору сервера)
cat privatekey | wg pubkey
```
!!! note "Частичная маршрутизация"
Если нужно направлять через VPN только трафик к GitHub, замените `AllowedIPs`:
```ini
AllowedIPs = 140.82.112.0/20, 185.199.108.0/22
```
!!! warning "Проблемы с MTU"
Если соединение установлено, но есть потери пакетов — уменьшите MTU в секции `[Interface]`:
```ini
MTU = 1420
```
---
### 3. Управление туннелем
```bash
# Поднять туннель
sudo wg-quick up wg0
# Проверить статус и статистику соединения
sudo wg show
# Остановить туннель
sudo wg-quick down wg0
```
---
### 4. Автозапуск при загрузке системы
```bash
sudo systemctl enable wg-quick@wg0
sudo systemctl start wg-quick@wg0
```
---
### 5. Проверка подключения
```bash
# Убедиться, что интерфейс поднят
ip addr show wg0
# Проверить маршруты
ip route show
# Пинг до VPN-сервера
ping 10.0.0.1
# Проверить внешний IP (должен совпадать с IP VPN-сервера)
curl -s ifconfig.me
```
После успешного подключения повторно запустите установку:
```bash
cobot setup
```
или только обновление rosdep:
```bash
rosdep update
```
---
### 6. Диагностика
Если туннель не поднимается, смотрите системные логи:
```bash
sudo journalctl -u wg-quick@wg0 -f
```
Убедитесь, что на **сервере** открыт UDP-порт `51820`:
```bash
# Проверить на сервере
sudo ufw status
# или
sudo iptables -L -n | grep 51820
```
+1
View File
@@ -84,5 +84,6 @@ nav:
- Решение проблем: - Решение проблем:
- Ошибка конфигурации: troubleshooting/config-error.md - Ошибка конфигурации: troubleshooting/config-error.md
- Ошибка SSL (rosdep/setup): troubleshooting/ssl-error.md
-10
View File
@@ -24,16 +24,6 @@ endpoints:
timeout: 5.0 timeout: 5.0
enabled: true enabled: true
- path: /robot/stop
method: POST
type: service
ros_name: cobot/stop
msg_type: std_srvs/srv/Trigger
summary: "Немедленно остановить движение"
description: "Вызывает сервис экстренной остановки — движение прерывается немедленно."
tags: [motion]
response_fields: [success, message]
timeout: 5.0
enabled: true enabled: true
- path: /robot/move/named - path: /robot/move/named
+11
View File
@@ -6,6 +6,7 @@ import uvicorn
from fastapi import FastAPI from fastapi import FastAPI
from fastmcp import FastMCP from fastmcp import FastMCP
from sensor_msgs.msg import JointState from sensor_msgs.msg import JointState
from std_srvs.srv import Trigger
from .dynamic_router import build_dynamic_router from .dynamic_router import build_dynamic_router
from .ros_node import CobotWebNode, get_bridge, set_bridge from .ros_node import CobotWebNode, get_bridge, set_bridge
@@ -47,6 +48,16 @@ def main():
app.include_router(positions.router) app.include_router(positions.router)
app.mount("/mcp", mcp_http) app.mount("/mcp", mcp_http)
@app.post("/stop", tags=["stop"], summary="Остановить всё: runner, траекторию и планировщик")
def stop_all():
runner.stop_if_running()
trajectory.send_stop_trajectory()
try:
result = get_bridge().call_service(Trigger, "cobot/stop", Trigger.Request())
return {"status": "stopped", "success": result.success, "message": result.message}
except RuntimeError:
return {"status": "stopped", "success": True, "message": "Планировщик не запущен"}
uvicorn.run(app, host=host, port=port) uvicorn.run(app, host=host, port=port)
+4 -8
View File
@@ -9,8 +9,6 @@ from typing import Optional
import yaml import yaml
from fastapi import APIRouter, Form, HTTPException, Query, UploadFile, File from fastapi import APIRouter, Form, HTTPException, Query, UploadFile, File
from std_srvs.srv import Trigger
from .ros_node import get_bridge from .ros_node import get_bridge
router = APIRouter(prefix="/sequences", tags=["sequences"]) router = APIRouter(prefix="/sequences", tags=["sequences"])
@@ -95,12 +93,12 @@ async def start_runner(
return {"status": "started", "pid": _process.pid, "config": filename} return {"status": "started", "pid": _process.pid, "config": filename}
@router.post("/stop", summary="Остановить motion_sequence_runner и послать cobot/stop") def stop_if_running() -> Optional[int]:
def stop_runner(): """Kill the runner process if it is running. Returns exit code or None if not running."""
global _process global _process
with _lock: with _lock:
if not _process or _process.poll() is not None: if not _process or _process.poll() is not None:
raise HTTPException(404, "Runner не запущен") return None
pgid = os.getpgid(_process.pid) pgid = os.getpgid(_process.pid)
os.killpg(pgid, signal.SIGTERM) os.killpg(pgid, signal.SIGTERM)
try: try:
@@ -108,10 +106,8 @@ def stop_runner():
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
os.killpg(pgid, signal.SIGKILL) os.killpg(pgid, signal.SIGKILL)
_process.wait() _process.wait()
code = _process.returncode return _process.returncode
result = get_bridge().call_service(Trigger, "cobot/stop", Trigger.Request())
return {"status": "stopped", "returncode": code, "success": result.success, "message": result.message}
@router.get("/status", summary="Статус motion_sequence_runner") @router.get("/status", summary="Статус motion_sequence_runner")
+2 -9
View File
@@ -5,7 +5,6 @@ from datetime import datetime
from builtin_interfaces.msg import Duration from builtin_interfaces.msg import Duration
from fastapi import APIRouter, File, HTTPException, Query, UploadFile from fastapi import APIRouter, File, HTTPException, Query, UploadFile
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from std_srvs.srv import Trigger
from trajectory_msgs.msg import JointTrajectory, JointTrajectoryPoint from trajectory_msgs.msg import JointTrajectory, JointTrajectoryPoint
from .config_loader import load_joint_limits, load_joint_names from .config_loader import load_joint_limits, load_joint_names
@@ -172,11 +171,9 @@ async def send_csv_trajectory(
return {"status": "sent", "points": len(rows), "filename": file.filename} return {"status": "sent", "points": len(rows), "filename": file.filename}
@router.post("/stop", summary="Остановить выполнение траектории") def send_stop_trajectory() -> None:
def stop_trajectory(): """Publish a hold-position (or empty) trajectory to freeze joint motion."""
bridge = get_bridge() bridge = get_bridge()
# Replace ongoing trajectory with single point at current position
joint_states = bridge.get_latest("/joint_states") joint_states = bridge.get_latest("/joint_states")
if joint_states is not None and len(joint_states.position) >= N_JOINTS: if joint_states is not None and len(joint_states.position) >= N_JOINTS:
current_positions = list(joint_states.position[:N_JOINTS]) current_positions = list(joint_states.position[:N_JOINTS])
@@ -189,10 +186,6 @@ def stop_trajectory():
_publish(msg) _publish(msg)
_log("[stop] joint_states недоступны, отправлена пустая траектория") _log("[stop] joint_states недоступны, отправлена пустая траектория")
# cobot/stop cancels MoveIt action-based motion
result = bridge.call_service(Trigger, "cobot/stop", Trigger.Request())
_log(f"[stop] cobot/stop -> success={result.success}, message={result.message}")
return {"status": "stopped", "success": result.success, "message": result.message}
@router.get("/logs", summary="Последние лог-записи траекторного модуля") @router.get("/logs", summary="Последние лог-записи траекторного модуля")