Merge branch 'dev'
This commit is contained in:
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
|
||||||
|
```
|
||||||
@@ -84,5 +84,6 @@ nav:
|
|||||||
|
|
||||||
- Решение проблем:
|
- Решение проблем:
|
||||||
- Ошибка конфигурации: troubleshooting/config-error.md
|
- Ошибка конфигурации: troubleshooting/config-error.md
|
||||||
|
- Ошибка SSL (rosdep/setup): troubleshooting/ssl-error.md
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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="Последние лог-записи траекторного модуля")
|
||||||
|
|||||||
Reference in New Issue
Block a user